Function bodies 330 total
findByIdOrThrow function · typescript · L44-L50 (7 LOC)packages/cli/src/repositories/specification.ts
export async function findByIdOrThrow(cwd: string, id: string): Promise<Specification> {
const specification = await findById(cwd, id);
if (!specification) {
throw new Error(`Specification ${id} not found.`);
}
return specification;
}findById function · typescript · L52-L66 (15 LOC)packages/cli/src/repositories/specification.ts
export async function findById(cwd: string, id: string): Promise<Specification | null> {
const specsDir = getSpecificationsDir(cwd);
const yamlPath = fs.joinPath(specsDir, `${id}.yaml`);
if (!(await fs.exists(yamlPath))) {
return null;
}
const raw = await fs.readYAML<unknown>(yamlPath);
const result = SpecificationSchema.safeParse(raw);
if (!result.success) {
throw new Error(`Validation error for specification ${id}:\n${formatZodError(result.error)}`);
}
return result.data;
}findAll function · typescript · L68-L83 (16 LOC)packages/cli/src/repositories/specification.ts
export async function findAll(cwd: string): Promise<Specification[]> {
const specsDir = getSpecificationsDir(cwd);
const files = await fs.readdirFiles(specsDir, (name) =>
/^spec-\d{6}\.yaml$/.test(name),
);
const specifications: Specification[] = [];
for (const file of files.sort()) {
const raw = await fs.readYAML<unknown>(fs.joinPath(specsDir, file));
const result = SpecificationSchema.safeParse(raw);
if (result.success) {
specifications.push(result.data);
}
}
return specifications;
}deleteById function · typescript · L85-L92 (8 LOC)packages/cli/src/repositories/specification.ts
export async function deleteById(cwd: string, id: string): Promise<void> {
const specsDir = getSpecificationsDir(cwd);
const yamlPath = fs.joinPath(specsDir, `${id}.yaml`);
const specDir = fs.joinPath(specsDir, id);
await fs.remove(yamlPath);
await fs.remove(specDir);
}startApproval function · typescript · L38-L105 (68 LOC)packages/cli/src/services/approval-service.ts
export async function startApproval(
cwd: string,
target: ApprovalTarget,
handler: ApprovalHandler,
options?: ApprovalOptions,
): Promise<ApprovalResult> {
// 1. Precondition check
if (target.status !== "draft") {
throw new Error(
`Cannot start approval: ${target.id} status is "${target.status}", expected "draft".`
);
}
const branchName = buildBranchName(target);
// 2. Dry-run mode
if (options?.dryRun) {
const prTitle = handler.buildPrTitle(target);
console.log(`[dry-run] Create branch: ${branchName}`);
console.log(`[dry-run] Status change: draft → approved`);
console.log(`[dry-run] Create PR: ${prTitle}`);
return { branchName, prNumber: 0, prUrl: "" };
}
// 3. Re-validate
await handler.revalidate(cwd, target);
// 4. Save original branch
const originalBranch = await gitRepo.getCurrentBranch(cwd);
try {
// 5. Create and switch to approval branch
await gitRepo.createBranch(cwd, branchName);
await gitRepo.cinitContext function · typescript · L14-L59 (46 LOC)packages/cli/src/services/context-service.ts
export async function initContext(
cwd: string,
options: InitContextOptions,
): Promise<ProjectContext> {
if (await contextRepo.contextExists(cwd)) {
throw new Error("context.yaml already exists.");
}
const now = new Date().toISOString();
const context: ProjectContext = {
id: options.id,
name: options.name,
version: "0.1.0",
language: options.language ?? "ja",
createdAt: now,
updatedAt: now,
files: {
product: { path: `${CONTEXT_DIR}/product.yaml`, format: "yaml" },
technical: { structured: `${CONTEXT_DIR}/technical.yaml` },
structure: { structured: `${CONTEXT_DIR}/structure.yaml` },
domain: [],
},
};
await contextRepo.save(cwd, context);
// Generate template files (skip if they already exist)
const contextDir = fs.getReqordDir(cwd, CONTEXT_DIR);
const templateFiles: Array<[string, unknown]> = [
["product.yaml", { name: options.name, vision: "", goals: [], targetUsers: [] }],
["technical.yaml",showContext function · typescript · L72-L104 (33 LOC)packages/cli/src/services/context-service.ts
export async function showContext(cwd: string): Promise<ShowContextResult> {
const context = await contextRepo.load(cwd);
if (!context) {
throw new Error(CONTEXT_NOT_FOUND);
}
const contextDir = fs.getReqordDir(cwd, CONTEXT_DIR);
async function resolveFileStatus(fileRef: unknown): Promise<{ exists: boolean; content?: unknown }> {
const fullPath = fs.getReqordDir(cwd, resolveFilePath(fileRef));
const fileExists = await fs.exists(fullPath);
const content = fileExists ? await safeReadYAML(fullPath) : undefined;
return { exists: fileExists, content };
}
const product = await resolveFileStatus(context.files.product);
const technical = await resolveFileStatus(context.files.technical);
const structure = await resolveFileStatus(context.files.structure);
const domainDir = fs.joinPath(contextDir, DOMAIN_DIR);
const domainFiles = await fs.readdirFiles(domainDir);
return {
context,
productExists: product.exists,
technicalExists: technicaRepobility · severity-and-effort ranking · https://repobility.com
updateContext function · typescript · L120-L156 (37 LOC)packages/cli/src/services/context-service.ts
export async function updateContext(
cwd: string,
options: UpdateContextOptions,
): Promise<UpdateContextResult> {
const before = await contextRepo.load(cwd);
if (!before) {
throw new Error(CONTEXT_NOT_FOUND);
}
const after: ProjectContext = {
...before,
updatedAt: new Date().toISOString(),
};
if (options.name !== undefined) after.name = options.name;
if (options.version !== undefined) after.version = options.version;
await contextRepo.save(cwd, after);
const updatedFiles: string[] = [];
const filePatches: Array<[contextRepo.ContextFileType, Record<string, unknown> | undefined]> = [
["product", options.productPatch],
["technical", options.technicalPatch],
["structure", options.structurePatch],
];
for (const [fileType, patch] of filePatches) {
if (patch) {
const existing = await contextRepo.loadContextFile(cwd, fileType) as Record<string, unknown> | null;
await contextRepo.saveContextFile(cwd, fileType, { ...existsafeReadYAML function · typescript · L158-L164 (7 LOC)packages/cli/src/services/context-service.ts
async function safeReadYAML(path: string): Promise<unknown> {
try {
return await fs.readYAML(path);
} catch {
return null;
}
}resolveFilePath function · typescript · L166-L176 (11 LOC)packages/cli/src/services/context-service.ts
export function resolveFilePath(
fileRef: unknown,
): string {
if (typeof fileRef === "string") return fileRef;
if (typeof fileRef === "object" && fileRef !== null) {
const obj = fileRef as Record<string, unknown>;
if (typeof obj.path === "string") return obj.path;
if (typeof obj.structured === "string") return obj.structured;
}
return "";
}determineCoverage function · typescript · L28-L37 (10 LOC)packages/cli/src/services/coverage-service.ts
export function determineCoverage(
specs: Array<{ status: Status }>,
): CoverageStatus {
if (specs.length === 0) return "not-covered";
const hasApprovedOrImplemented = specs.some(
(s) => s.status === "approved" || s.status === "implemented",
);
if (hasApprovedOrImplemented) return "covered";
return "partial";
}buildCoverageReport function · typescript · L39-L74 (36 LOC)packages/cli/src/services/coverage-service.ts
export function buildCoverageReport(
requirements: Requirement[],
specifications: Specification[],
): CoverageReport {
const activeReqs = requirements.filter(
(r) => r.status !== "deprecated",
);
const coverages: RequirementCoverage[] = activeReqs.map((req) => {
const relatedSpecs = specifications
.filter(
(s) => s.requirementId === req.id && s.status !== "deprecated",
)
.map((s) => ({ id: s.id, status: s.status as Status }));
return {
requirementId: req.id,
title: req.title,
status: determineCoverage(relatedSpecs),
specifications: relatedSpecs,
};
});
const summary = {
covered: coverages.filter((c) => c.status === "covered").length,
partial: coverages.filter((c) => c.status === "partial").length,
notCovered: coverages.filter((c) => c.status === "not-covered").length,
total: coverages.length,
};
return {
requirements: coverages,
summary,
analyzedAt: new Date().toISOStringanalyzeRequirementCoverage function · typescript · L76-L90 (15 LOC)packages/cli/src/services/coverage-service.ts
export async function analyzeRequirementCoverage(
cwd: string,
requirementId?: string,
): Promise<CoverageReport> {
let requirements: Requirement[];
if (requirementId) {
const req = await reqRepo.findByIdOrThrow(cwd, requirementId);
requirements = [req];
} else {
requirements = await reqRepo.findAll(cwd);
}
const specifications = await specRepo.findAll(cwd);
return buildCoverageReport(requirements, specifications);
}buildPrBody function · typescript · L33-L71 (39 LOC)packages/cli/src/services/draft-reversion-service.ts
function buildPrBody(
id: string,
title: string,
version: string,
previousStatus: string,
impactedRequirements: string[],
entityType: "requirement" | "specification",
): string {
const isSpec = entityType === "specification";
const entityLabel = isSpec ? "Specification" : "Requirement";
const heading = isSpec ? "Specification Reversion to Draft" : "Requirement Reversion to Draft";
const impactLabel = isSpec
? "The following requirements depend on this specification's parent requirement:"
: "The following requirements depend on this requirement:";
const noImpactLabel = "None (no impact on other requirements)";
const impactSection =
impactedRequirements.length > 0
? impactedRequirements.map((rid) => `- ${rid}`).join("\n")
: noImpactLabel;
return `## ${heading}
| Field | Value |
|-----------|------|
| ID | ${id} |
| Title | ${title} |
| Version | ${version} |
| Previous Status | ${previousStatus} |
### Impact Analysis
${impactLabel}
${analyzeSpecImpact function · typescript · L99-L109 (11 LOC)packages/cli/src/services/draft-reversion-service.ts
async function analyzeSpecImpact(
cwd: string,
impact: ImpactAnalysis,
): Promise<string[]> {
// For specifications, analyze impact via the parent requirement
if (impact.parentRequirement) {
const parentImpact = await analyzeImpact(cwd, impact.parentRequirement.id);
return extractImpactedRequirements(parentImpact);
}
return [];
}All rows scored by the Repobility analyzer (https://repobility.com)
revertToDraft function · typescript · L111-L188 (78 LOC)packages/cli/src/services/draft-reversion-service.ts
export async function revertToDraft(
cwd: string,
id: string,
options?: DraftReversionOptions,
): Promise<DraftReversionResult> {
// 1. Load entity and check precondition
const entity = await loadEntity(cwd, id);
if (entity.status === "draft") {
throw new Error(`Cannot revert to draft: ${id} is already in draft status.`);
}
const previousStatus = entity.status;
const { type: entityType, dir } = getEntityInfo(id);
// 2. Analyze impact
const impact = await analyzeImpact(cwd, id);
const impactedRequirements = entityType === "specification"
? await analyzeSpecImpact(cwd, impact)
: extractImpactedRequirements(impact);
// 3. Dry-run mode
if (options?.dryRun) {
return { previousStatus, impactedRequirements };
}
// 4. Save original branch
const originalBranch = await gitRepo.getCurrentBranch(cwd);
const branchName = buildBranchName(id);
const filePath = `${REQORD_DIR}/${dir}/${id}.yaml`;
try {
// 5. Create and switch to reversionlistFeedbacks function · typescript · L40-L55 (16 LOC)packages/cli/src/services/feedback-service.ts
export async function listFeedbacks(
cwd: string,
options: ListFeedbacksOptions = {},
): Promise<FeedbackEntry[]> {
const index = await feedbackRepo.loadIndex(cwd);
let feedbacks = index.feedbacks;
if (options.state && options.state !== "all") {
feedbacks = feedbacks.filter((f) => f.status === options.state);
}
if (options.type) {
feedbacks = feedbacks.filter((f) => f.type === options.type);
}
return feedbacks;
}showFeedback function · typescript · L57-L72 (16 LOC)packages/cli/src/services/feedback-service.ts
export async function showFeedback(
cwd: string,
issueNumber: number,
): Promise<ShowFeedbackResult> {
const index = await feedbackRepo.loadIndex(cwd);
const feedback = index.feedbacks.find((f) => f.githubIssue === issueNumber);
if (!feedback) {
throw new Error(
`Feedback for issue #${issueNumber} not found in index.yaml. Run 'reqord feedback sync' first.`,
);
}
const issue = await githubClient.getIssue(issueNumber);
return { feedback, issue };
}linkToRequirement function · typescript · L74-L93 (20 LOC)packages/cli/src/services/feedback-service.ts
export async function linkToRequirement(
cwd: string,
options: LinkToRequirementOptions,
): Promise<void> {
// Verify requirement exists
await reqRepo.findByIdOrThrow(cwd, options.requirementId);
// Update index.yaml
const index = await feedbackRepo.loadIndex(cwd);
const feedback = getOrCreateFeedback(index, options.issueNumber, options);
if (!feedback.linkedTo.requirements.includes(options.requirementId)) {
feedback.linkedTo.requirements.push(options.requirementId);
}
await feedbackRepo.saveIndex(cwd, index);
// Update GitHub Issue body with HTML comment
await updateGitHubIssueBody(options.issueNumber, feedback);
}linkWithNewRequirement function · typescript · L95-L126 (32 LOC)packages/cli/src/services/feedback-service.ts
export async function linkWithNewRequirement(
cwd: string,
options: LinkWithNewRequirementOptions,
): Promise<string> {
// Get GitHub Issue for title
const issue = await githubClient.getIssue(options.issueNumber);
// Create new Requirement
const result = await createRequirement(cwd, {
title: `[Feedback #${options.issueNumber}] ${issue.title}`,
priority: "medium",
});
const newId = result.requirement.id;
// Add origin info
const requirement = result.requirement;
requirement.origin = { feedbackIssue: options.issueNumber };
await reqRepo.save(cwd, requirement);
// Update index.yaml
const index = await feedbackRepo.loadIndex(cwd);
const feedback = getOrCreateFeedback(index, options.issueNumber, options);
feedback.linkedTo.createdRequirements.push(newId);
await feedbackRepo.saveIndex(cwd, index);
// Update GitHub Issue body with HTML comment
await updateGitHubIssueBody(options.issueNumber, feedback);
return newId;
}linkToSpecification function · typescript · L128-L147 (20 LOC)packages/cli/src/services/feedback-service.ts
export async function linkToSpecification(
cwd: string,
options: LinkToSpecificationOptions,
): Promise<void> {
// Verify specification exists
await specRepo.findByIdOrThrow(cwd, options.specificationId);
// Update index.yaml
const index = await feedbackRepo.loadIndex(cwd);
const feedback = getOrCreateFeedback(index, options.issueNumber, options);
if (!feedback.linkedTo.specifications.includes(options.specificationId)) {
feedback.linkedTo.specifications.push(options.specificationId);
}
await feedbackRepo.saveIndex(cwd, index);
// Update GitHub Issue body with HTML comment
await updateGitHubIssueBody(options.issueNumber, feedback);
}closeFeedback function · typescript · L149-L165 (17 LOC)packages/cli/src/services/feedback-service.ts
export async function closeFeedback(
cwd: string,
issueNumber: number,
): Promise<void> {
const index = await feedbackRepo.loadIndex(cwd);
const feedback = index.feedbacks.find((f) => f.githubIssue === issueNumber);
if (!feedback) {
throw new Error(`Feedback for issue #${issueNumber} not found`);
}
feedback.status = "closed";
await feedbackRepo.saveIndex(cwd, index);
const summary = buildImpactSummary(feedback);
await githubClient.closeIssue(issueNumber, summary);
}updateGitHubIssueBody function · typescript · L167-L181 (15 LOC)packages/cli/src/services/feedback-service.ts
async function updateGitHubIssueBody(
issueNumber: number,
feedback: FeedbackEntry,
): Promise<void> {
const issue = await githubClient.getIssue(issueNumber);
const currentBody = issue.body ?? "";
const newBody = upsertReqordComment(currentBody, {
type: feedback.type,
severity: feedback.severity,
linkedTo: feedback.linkedTo,
});
if (newBody !== currentBody) {
await githubClient.updateIssueBody(issueNumber, newBody);
}
}Repobility · code-quality intelligence · https://repobility.com
createEmptyFeedbackEntry function · typescript · L183-L195 (13 LOC)packages/cli/src/services/feedback-service.ts
function createEmptyFeedbackEntry(issueNumber: number): FeedbackEntry {
return {
githubIssue: issueNumber,
linkedTo: {
requirements: [],
createdRequirements: [],
specifications: [],
createdSpecifications: [],
},
syncedAt: new Date().toISOString(),
status: "open",
};
}getOrCreateFeedback function · typescript · L197-L210 (14 LOC)packages/cli/src/services/feedback-service.ts
function getOrCreateFeedback(
index: { feedbacks: FeedbackEntry[] },
issueNumber: number,
options: { type?: FeedbackType; severity?: FeedbackSeverity },
): FeedbackEntry {
let feedback = index.feedbacks.find((f) => f.githubIssue === issueNumber);
if (!feedback) {
feedback = createEmptyFeedbackEntry(issueNumber);
index.feedbacks.push(feedback);
}
if (options.type) feedback.type = options.type;
if (options.severity) feedback.severity = options.severity;
return feedback;
}buildImpactSummary function · typescript · L212-L228 (17 LOC)packages/cli/src/services/feedback-service.ts
function buildImpactSummary(feedback: FeedbackEntry): string {
const lines = ["**Feedback closed - Impact summary:**", ""];
const sections: Array<{ label: string; ids: string[] }> = [
{ label: "Linked Requirements", ids: feedback.linkedTo.requirements },
{ label: "Created Requirements", ids: feedback.linkedTo.createdRequirements },
{ label: "Linked Specifications", ids: feedback.linkedTo.specifications },
{ label: "Created Specifications", ids: feedback.linkedTo.createdSpecifications },
];
for (const { label, ids } of sections) {
if (ids.length > 0) {
lines.push(`- ${label}: ${ids.join(", ")}`);
}
}
return lines.join("\n");
}feedbackTypeToLabel function · typescript · L240-L249 (10 LOC)packages/cli/src/services/feedback-service.ts
function feedbackTypeToLabel(type: FeedbackType): string {
const map: Record<FeedbackType, string> = {
"requirement-gap": "requirement-gap (missing requirement)",
"spec-mismatch": "spec-mismatch (spec vs implementation mismatch)",
"bug": "implementation-bug (implementation bug)",
"improvement": "improvement (improvement proposal)",
"security": "security (security issue)",
};
return map[type] ?? type;
}severityToLabel function · typescript · L251-L259 (9 LOC)packages/cli/src/services/feedback-service.ts
function severityToLabel(severity: FeedbackSeverity): string {
const map: Record<FeedbackSeverity, string> = {
critical: "critical (affects all users)",
high: "high (affects many users)",
medium: "medium (affects some users)",
low: "low (minor issue)",
};
return map[severity] ?? severity;
}buildFeedbackIssueBody function · typescript · L261-L299 (39 LOC)packages/cli/src/services/feedback-service.ts
function buildFeedbackIssueBody(options: CreateFeedbackOptions): string {
const lines: string[] = [];
lines.push("### What happened? / What did you notice?");
lines.push("");
lines.push(options.description);
lines.push("");
lines.push("### Feedback type");
lines.push("");
const typeLabel = options.type
? feedbackTypeToLabel(options.type)
: "unknown/unclassified";
lines.push(typeLabel);
lines.push("");
if (options.relatedReq) {
lines.push("### Related requirement (Requirement)");
lines.push("");
lines.push(options.relatedReq);
lines.push("");
}
if (options.relatedSpec) {
lines.push("### Related specification (Specification)");
lines.push("");
lines.push(options.relatedSpec);
lines.push("");
}
if (options.severity) {
lines.push("### Severity");
lines.push("");
lines.push(severityToLabel(options.severity));
lines.push("");
}
return lines.join("\n");
}createFeedbackIssue function · typescript · L301-L335 (35 LOC)packages/cli/src/services/feedback-service.ts
export async function createFeedbackIssue(
cwd: string,
options: CreateFeedbackOptions,
): Promise<number> {
const body = buildFeedbackIssueBody(options);
const title = options.title.startsWith("[Feedback]")
? options.title
: `[Feedback] ${options.title}`;
const result = await githubClient.createIssue({
title,
body,
labels: ["feedback", "reqord-generated", ...(options.type ? [options.type] : [])],
});
const index = await feedbackRepo.loadIndex(cwd);
const newEntry: FeedbackEntry = {
githubIssue: result.number,
type: options.type,
severity: options.severity,
linkedTo: {
requirements: [],
createdRequirements: [],
specifications: [],
createdSpecifications: [],
},
syncedAt: new Date().toISOString(),
status: "open",
};
index.feedbacks.push(newEntry);
await feedbackRepo.saveIndex(cwd, index);
return result.number;
}resolveFeedback function · typescript · L343-L393 (51 LOC)packages/cli/src/services/feedback-service.ts
export async function resolveFeedback(
cwd: string,
options: ResolveFeedbackOptions,
): Promise<void> {
const index = await feedbackRepo.loadIndex(cwd);
const feedback = index.feedbacks.find(
(f) => f.githubIssue === options.issueNumber,
);
if (!feedback) {
throw new Error(
`Feedback for issue #${options.issueNumber} not found in index.yaml`,
);
}
const isReq = options.artifactId.startsWith("req-");
const isSpec = options.artifactId.startsWith("spec-");
if (!isReq && !isSpec) {
throw new Error(
`Invalid artifact ID: ${options.artifactId}. Must start with "req-" or "spec-"`,
);
}
// Verify artifact is linked (check both linked and created lists)
const linkedList = isReq
? feedback.linkedTo.requirements
: feedback.linkedTo.specifications;
const createdList = isReq
? feedback.linkedTo.createdRequirements
: feedback.linkedTo.createdSpecifications;
if (!linkedList.includes(options.artifactId) && !createdList.Repobility analyzer · published findings · https://repobility.com
checkRemainingFlags function · typescript · L402-L437 (36 LOC)packages/cli/src/services/feedback-service.ts
export function checkRemainingFlags(
feedback: FeedbackEntry,
): RemainingFlag[] {
const remaining: RemainingFlag[] = [];
const resolvedReqs = new Set(feedback.linkedTo.resolved?.requirements ?? []);
const resolvedSpecs = new Set(feedback.linkedTo.resolved?.specifications ?? []);
const allReqs = [
...feedback.linkedTo.requirements,
...(feedback.linkedTo.createdRequirements ?? []),
];
const allSpecs = [
...feedback.linkedTo.specifications,
...(feedback.linkedTo.createdSpecifications ?? []),
];
for (const reqId of allReqs) {
if (!resolvedReqs.has(reqId)) {
remaining.push({
artifactId: reqId,
issueNumber: feedback.githubIssue,
severity: feedback.severity ?? "medium",
});
}
}
for (const specId of allSpecs) {
if (!resolvedSpecs.has(specId)) {
remaining.push({
artifactId: specId,
issueNumber: feedback.githubIssue,
severity: feedback.severity ?? "medium",
});
}
}
unlinkFromRequirement function · typescript · L450-L482 (33 LOC)packages/cli/src/services/feedback-service.ts
export async function unlinkFromRequirement(
cwd: string,
options: UnlinkFromRequirementOptions,
): Promise<void> {
const index = await feedbackRepo.loadIndex(cwd);
const feedback = index.feedbacks.find((f) => f.githubIssue === options.issueNumber);
if (!feedback) {
throw new Error(`Feedback for issue #${options.issueNumber} not found in index.yaml`);
}
// Remove from linkedTo.requirements
const reqIndex = feedback.linkedTo.requirements.indexOf(options.requirementId);
if (reqIndex === -1) {
throw new Error(
`${options.requirementId} is not linked to feedback #${options.issueNumber}`,
);
}
feedback.linkedTo.requirements.splice(reqIndex, 1);
// Also remove from resolved to avoid stale resolved entries on re-link
if (feedback.linkedTo.resolved?.requirements) {
const resolvedIdx = feedback.linkedTo.resolved.requirements.indexOf(options.requirementId);
if (resolvedIdx !== -1) {
feedback.linkedTo.resolved.requirements.splice(resolvunlinkFromSpecification function · typescript · L484-L516 (33 LOC)packages/cli/src/services/feedback-service.ts
export async function unlinkFromSpecification(
cwd: string,
options: UnlinkFromSpecificationOptions,
): Promise<void> {
const index = await feedbackRepo.loadIndex(cwd);
const feedback = index.feedbacks.find((f) => f.githubIssue === options.issueNumber);
if (!feedback) {
throw new Error(`Feedback for issue #${options.issueNumber} not found in index.yaml`);
}
// Remove from linkedTo.specifications
const specIndex = feedback.linkedTo.specifications.indexOf(options.specificationId);
if (specIndex === -1) {
throw new Error(
`${options.specificationId} is not linked to feedback #${options.issueNumber}`,
);
}
feedback.linkedTo.specifications.splice(specIndex, 1);
// Also remove from resolved to avoid stale resolved entries on re-link
if (feedback.linkedTo.resolved?.specifications) {
const resolvedIdx = feedback.linkedTo.resolved.specifications.indexOf(options.specificationId);
if (resolvedIdx !== -1) {
feedback.linkedTo.resolved.spesyncFromGitHub function · typescript · L8-L29 (22 LOC)packages/cli/src/services/feedback-sync-service.ts
export async function syncFromGitHub(cwd: string): Promise<number> {
const issues = await githubClient.listFeedbackIssues();
const index = await feedbackRepo.loadIndex(cwd);
let updatedCount = 0;
for (const issue of issues) {
const fromGitHub = parseGitHubIssue(issue);
const existingIdx = index.feedbacks.findIndex(
(f) => f.githubIssue === issue.number,
);
if (existingIdx >= 0) {
index.feedbacks[existingIdx] = mergeFeedback(index.feedbacks[existingIdx], fromGitHub);
} else {
index.feedbacks.push(fromGitHub);
}
updatedCount++;
}
await feedbackRepo.saveIndex(cwd, index);
return updatedCount;
}mergeFeedback function · typescript · L32-L45 (14 LOC)packages/cli/src/services/feedback-sync-service.ts
export function mergeFeedback(
existing: FeedbackEntry,
fromGitHub: FeedbackEntry,
): FeedbackEntry {
return {
githubIssue: existing.githubIssue,
title: fromGitHub.title,
type: existing.type ?? fromGitHub.type,
severity: existing.severity ?? fromGitHub.severity,
linkedTo: existing.linkedTo,
syncedAt: fromGitHub.syncedAt,
status: fromGitHub.status,
};
}syncToGitHub function · typescript · L47-L64 (18 LOC)packages/cli/src/services/feedback-sync-service.ts
export async function syncToGitHub(cwd: string): Promise<number> {
const index = await feedbackRepo.loadIndex(cwd);
let count = 0;
for (const feedback of index.feedbacks) {
const issue = await githubClient.getIssue(feedback.githubIssue);
const metadata = {
type: feedback.type,
severity: feedback.severity,
linkedTo: feedback.linkedTo,
};
const newBody = upsertReqordComment(issue.body ?? "", metadata);
if (newBody !== issue.body) {
await githubClient.updateIssueBody(feedback.githubIssue, newBody);
count++;
}
}
return count;
}parseGitHubIssue function · typescript · L66-L83 (18 LOC)packages/cli/src/services/feedback-sync-service.ts
export function parseGitHubIssue(issue: GitHubIssue): FeedbackEntry {
const comment = parseReqordComment(issue.body ?? "");
return {
githubIssue: issue.number,
title: issue.title,
type: comment?.type,
severity: comment?.severity,
linkedTo: comment?.linkedTo ?? {
requirements: [],
createdRequirements: [],
specifications: [],
createdSpecifications: [],
},
syncedAt: new Date().toISOString(),
status: issue.state === "closed" ? "closed" : "open",
};
}normalizeIssue function · typescript · L30-L39 (10 LOC)packages/cli/src/services/github-client.ts
function normalizeIssue(raw: GitHubIssueRaw): GitHubIssue {
return {
number: raw.number,
title: raw.title,
state: raw.state === "CLOSED" ? "closed" : "open",
labels: raw.labels.map((l) => l.name),
createdAt: raw.createdAt,
body: raw.body,
};
}Repobility · severity-and-effort ranking · https://repobility.com
runGh function · typescript · L41-L63 (23 LOC)packages/cli/src/services/github-client.ts
function runGh(args: string[], options?: { stdin?: string }): Promise<string> {
return new Promise((resolve, reject) => {
const proc = spawn("gh", args);
let stdout = "";
let stderr = "";
proc.stdout.on("data", (chunk: Buffer) => { stdout += chunk.toString(); });
proc.stderr.on("data", (chunk: Buffer) => { stderr += chunk.toString(); });
if (options?.stdin !== undefined) {
proc.stdin.write(options.stdin);
proc.stdin.end();
}
proc.on("close", (code) => {
if (code !== 0) {
reject(new Error(`gh ${args[0]} ${args[1] ?? ""} failed (code ${code}): ${stderr}`.trim()));
return;
}
resolve(stdout);
});
proc.on("error", reject);
});
}listFeedbackIssues function · typescript · L65-L74 (10 LOC)packages/cli/src/services/github-client.ts
export async function listFeedbackIssues(): Promise<GitHubIssue[]> {
const stdout = await runGh([
"issue", "list",
"--label", "feedback",
"--json", "number,title,state,labels,createdAt,body",
"--limit", "1000",
]);
const raw: GitHubIssueRaw[] = JSON.parse(stdout);
return raw.map(normalizeIssue);
}listIssuesByLabel function · typescript · L76-L89 (14 LOC)packages/cli/src/services/github-client.ts
export async function listIssuesByLabel(
labels: string[],
state: "open" | "closed" | "all" = "all",
): Promise<GitHubIssue[]> {
const args = ["issue", "list"];
for (const label of labels) {
args.push("--label", label);
}
args.push("--state", state, "--json", "number,title,state,labels,createdAt,body", "--limit", "1000");
const stdout = await runGh(args);
const raw: GitHubIssueRaw[] = JSON.parse(stdout);
return raw.map(normalizeIssue);
}getIssue function · typescript · L91-L98 (8 LOC)packages/cli/src/services/github-client.ts
export async function getIssue(issueNumber: number): Promise<GitHubIssue> {
const stdout = await runGh([
"issue", "view", String(issueNumber),
"--json", "number,title,state,labels,createdAt,body",
]);
const raw: GitHubIssueRaw = JSON.parse(stdout);
return normalizeIssue(raw);
}getIssueDetail function · typescript · L100-L111 (12 LOC)packages/cli/src/services/github-client.ts
export async function getIssueDetail(issueNumber: number): Promise<GitHubIssueDetail> {
const stdout = await runGh([
"issue", "view", String(issueNumber),
"--json", "number,title,state,labels,createdAt,body,updatedAt,closedAt",
]);
const raw = JSON.parse(stdout);
return {
...normalizeIssue(raw),
updatedAt: raw.updatedAt,
closedAt: raw.closedAt ?? null,
};
}updateIssueBody function · typescript · L113-L118 (6 LOC)packages/cli/src/services/github-client.ts
export async function updateIssueBody(
issueNumber: number,
newBody: string,
): Promise<void> {
await runGh(["issue", "edit", String(issueNumber), "--body-file", "-"], { stdin: newBody });
}closeIssue function · typescript · L120-L129 (10 LOC)packages/cli/src/services/github-client.ts
export async function closeIssue(
issueNumber: number,
comment?: string,
): Promise<void> {
const args = ["issue", "close", String(issueNumber)];
if (comment) {
args.push("--comment", comment);
}
await runGh(args);
}listAllIssues function · typescript · L142-L156 (15 LOC)packages/cli/src/services/github-client.ts
export async function listAllIssues(
state: "open" | "closed" | "all" = "all",
limit: number = 500,
): Promise<GitHubIssue[]> {
const args = [
"issue", "list",
"--state", state,
"--json", "number,title,state,labels,createdAt,body",
"--limit", String(limit),
];
const stdout = await runGh(args);
const raw: GitHubIssueRaw[] = JSON.parse(stdout);
return raw.map(normalizeIssue);
}All rows scored by the Repobility analyzer (https://repobility.com)
createIssue function · typescript · L163-L188 (26 LOC)packages/cli/src/services/github-client.ts
export async function createIssue(
options: CreateIssueOptions,
): Promise<CreatedIssue> {
const args = [
"issue",
"create",
"--title",
options.title,
"--label",
options.labels.join(","),
"--body-file",
"-",
];
const stdout = await runGh(args, { stdin: options.body });
// gh issue create outputs the URL like: https://github.com/owner/repo/issues/123
const match = stdout.trim().match(/https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/(\d+)/);
if (!match) {
throw new Error(`Failed to extract issue URL from gh output: ${stdout}`);
}
return {
number: parseInt(match[1], 10),
url: match[0],
};
}loadTasksForSpec function · typescript · L42-L50 (9 LOC)packages/cli/src/services/impact-service.ts
async function loadTasksForSpec(cwd: string, specificationId: string): Promise<TaskEntry[]> {
const tasksPath = fs.joinPath(cwd, REQORD_DIR, ISSUES_DIR, "tasks.yaml");
if (!(await fs.exists(tasksPath))) {
return [];
}
const raw = await fs.readYAML<unknown>(tasksPath);
const parsed = TasksIndexSchema.parse(raw);
return parsed.tasks.filter((t) => t.linkedTo.specifications.includes(specificationId));
}loadAllTasks function · typescript · L52-L60 (9 LOC)packages/cli/src/services/impact-service.ts
async function loadAllTasks(cwd: string): Promise<TaskEntry[]> {
const tasksPath = fs.joinPath(cwd, REQORD_DIR, ISSUES_DIR, "tasks.yaml");
if (!(await fs.exists(tasksPath))) {
return [];
}
const raw = await fs.readYAML<unknown>(tasksPath);
const parsed = TasksIndexSchema.parse(raw);
return parsed.tasks;
}