Add public API for methods and values
This commit is contained in:
@@ -66,6 +66,14 @@ export const METHOD_FACET_API_SECTION_IDS = [
|
||||
|
||||
export type MethodFacetApiSectionId = (typeof METHOD_FACET_API_SECTION_IDS)[number];
|
||||
|
||||
/** `GET /api/create-flow/methods?section=` — four method decks + core values (CR-115). */
|
||||
export const CATALOG_SECTION_IDS = [
|
||||
...METHOD_FACET_API_SECTION_IDS,
|
||||
"coreValues",
|
||||
] as const;
|
||||
|
||||
export type CatalogSectionId = (typeof CATALOG_SECTION_IDS)[number];
|
||||
|
||||
export type CustomRuleFacetKind = "coreValues" | "method";
|
||||
|
||||
export type CustomRuleFacetRow = {
|
||||
|
||||
@@ -131,13 +131,60 @@ export async function fetchRankedTemplatesByFacets(options: {
|
||||
return { templates, scores: parseScoresPayload(data.scores) };
|
||||
}
|
||||
|
||||
export type TemplateDetailDto = {
|
||||
template: RuleTemplateDto;
|
||||
methods: Array<{ section: string; slug: string }>;
|
||||
};
|
||||
|
||||
export async function fetchTemplateDetailBySlug(
|
||||
slug: string,
|
||||
options?: FetchTemplatesOptions,
|
||||
): Promise<TemplateDetailDto | null | { error: string }> {
|
||||
const encoded = encodeURIComponent(slug);
|
||||
try {
|
||||
const res = await fetch(`/api/templates/${encoded}`, {
|
||||
credentials: "include",
|
||||
signal: options?.signal,
|
||||
});
|
||||
const data = (await res.json()) as TemplateDetailDto & {
|
||||
error?: string | { message?: string };
|
||||
};
|
||||
if (!res.ok) {
|
||||
if (res.status === 404) return null;
|
||||
const err = data.error;
|
||||
const message =
|
||||
typeof err === "string"
|
||||
? err
|
||||
: err && typeof err === "object" && typeof err.message === "string"
|
||||
? err.message
|
||||
: "Could not load template";
|
||||
return { error: message };
|
||||
}
|
||||
if (!data.template || typeof data.template.slug !== "string") {
|
||||
return { error: "Could not load template" };
|
||||
}
|
||||
return {
|
||||
template: data.template,
|
||||
methods: Array.isArray(data.methods) ? data.methods : [],
|
||||
};
|
||||
} catch (e) {
|
||||
if (isAbortError(e)) {
|
||||
throw new DOMException("Aborted", "AbortError");
|
||||
}
|
||||
return { error: "Could not load template" };
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchTemplateBySlug(
|
||||
slug: string,
|
||||
options?: FetchTemplatesOptions,
|
||||
): Promise<RuleTemplateDto | null | { error: string }> {
|
||||
const result = await fetchTemplates(options);
|
||||
const result = await fetchTemplateDetailBySlug(slug, options);
|
||||
if ("error" in result) {
|
||||
return result;
|
||||
}
|
||||
return result.find((t) => t.slug === slug) ?? null;
|
||||
if (result === null) {
|
||||
return null;
|
||||
}
|
||||
return result.template;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Public catalog DTOs for built-in governance methods and core values (CR-115).
|
||||
* Source of truth: `messages/en/create/customRule/*.json` (English v1).
|
||||
*/
|
||||
|
||||
import communicationMessages from "../../messages/en/create/customRule/communication.json";
|
||||
import conflictManagementMessages from "../../messages/en/create/customRule/conflictManagement.json";
|
||||
import coreValuesMessages from "../../messages/en/create/customRule/coreValues.json";
|
||||
import decisionApproachesMessages from "../../messages/en/create/customRule/decisionApproaches.json";
|
||||
import membershipMessages from "../../messages/en/create/customRule/membership.json";
|
||||
import type { MethodFacetApiSectionId } from "../create/customRuleFacets";
|
||||
|
||||
export type CatalogMethodDto = {
|
||||
/** Stable id — same as `methods[].id` in messages and `MethodFacet.slug`. */
|
||||
slug: string;
|
||||
label: string;
|
||||
/** Card copy from messages `supportText`. */
|
||||
description: string;
|
||||
/** Section-specific modal blocks from messages `sections`. */
|
||||
sections: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type CatalogCoreValueDto = {
|
||||
/** 1-based position in `coreValues.json` (`"1"`, `"2"`, …). */
|
||||
id: string;
|
||||
label: string;
|
||||
meaning: string;
|
||||
signals: string;
|
||||
};
|
||||
|
||||
export type CatalogSectionId = MethodFacetApiSectionId | "coreValues";
|
||||
|
||||
type MethodMessagesSource = {
|
||||
methods?: unknown;
|
||||
};
|
||||
|
||||
const METHOD_MESSAGES_BY_SECTION: Record<MethodFacetApiSectionId, unknown> = {
|
||||
communication: communicationMessages,
|
||||
membership: membershipMessages,
|
||||
decisionApproaches: decisionApproachesMessages,
|
||||
conflictManagement: conflictManagementMessages,
|
||||
};
|
||||
|
||||
function readMethodRows(source: unknown): Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
supportText?: string;
|
||||
sections?: Record<string, unknown>;
|
||||
}> {
|
||||
if (!source || typeof source !== "object") return [];
|
||||
const methods = (source as MethodMessagesSource).methods;
|
||||
if (!Array.isArray(methods)) return [];
|
||||
const out: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
supportText?: string;
|
||||
sections?: Record<string, unknown>;
|
||||
}> = [];
|
||||
for (const raw of methods) {
|
||||
if (!raw || typeof raw !== "object") continue;
|
||||
const o = raw as Record<string, unknown>;
|
||||
if (typeof o.id !== "string" || typeof o.label !== "string") continue;
|
||||
out.push({
|
||||
id: o.id,
|
||||
label: o.label,
|
||||
supportText:
|
||||
typeof o.supportText === "string" ? o.supportText : undefined,
|
||||
sections:
|
||||
o.sections && typeof o.sections === "object"
|
||||
? (o.sections as Record<string, unknown>)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function rowToCatalogMethod(row: {
|
||||
id: string;
|
||||
label: string;
|
||||
supportText?: string;
|
||||
sections?: Record<string, unknown>;
|
||||
}): CatalogMethodDto {
|
||||
return {
|
||||
slug: row.id,
|
||||
label: row.label,
|
||||
description: row.supportText ?? "",
|
||||
sections: row.sections ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
/** All built-in methods for a card-deck section, in messages authoring order. */
|
||||
export function listCatalogMethods(
|
||||
section: MethodFacetApiSectionId,
|
||||
): CatalogMethodDto[] {
|
||||
return readMethodRows(METHOD_MESSAGES_BY_SECTION[section]).map(
|
||||
rowToCatalogMethod,
|
||||
);
|
||||
}
|
||||
|
||||
export function getCatalogMethod(
|
||||
section: MethodFacetApiSectionId,
|
||||
slug: string,
|
||||
): CatalogMethodDto | null {
|
||||
const row = readMethodRows(METHOD_MESSAGES_BY_SECTION[section]).find(
|
||||
(r) => r.id === slug,
|
||||
);
|
||||
return row ? rowToCatalogMethod(row) : null;
|
||||
}
|
||||
|
||||
/** All preset core values, in `coreValues.json` order (ids `"1"` … `"n"`). */
|
||||
export function listCatalogCoreValues(): CatalogCoreValueDto[] {
|
||||
const values = (coreValuesMessages as { values?: unknown }).values;
|
||||
if (!Array.isArray(values)) return [];
|
||||
const out: CatalogCoreValueDto[] = [];
|
||||
for (let i = 0; i < values.length; i++) {
|
||||
const row = values[i];
|
||||
const id = String(i + 1);
|
||||
if (typeof row === "string") {
|
||||
out.push({ id, label: row, meaning: "", signals: "" });
|
||||
continue;
|
||||
}
|
||||
if (!row || typeof row !== "object") continue;
|
||||
const o = row as Record<string, unknown>;
|
||||
if (typeof o.label !== "string") continue;
|
||||
out.push({
|
||||
id,
|
||||
label: o.label,
|
||||
meaning: typeof o.meaning === "string" ? o.meaning : "",
|
||||
signals: typeof o.signals === "string" ? o.signals : "",
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function getCatalogCoreValue(id: string): CatalogCoreValueDto | null {
|
||||
return listCatalogCoreValues().find((v) => v.id === id) ?? null;
|
||||
}
|
||||
|
||||
/** Slugs for parity tests — same set as messages `methods[].id`. */
|
||||
export function catalogMethodSlugsForSection(
|
||||
section: MethodFacetApiSectionId,
|
||||
): string[] {
|
||||
return listCatalogMethods(section).map((m) => m.slug);
|
||||
}
|
||||
@@ -45,6 +45,23 @@ export async function listRuleTemplatesFromDb(): Promise<RuleTemplateDto[]> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Single curated template by slug; `null` when missing or DB unavailable. */
|
||||
export async function getRuleTemplateBySlug(
|
||||
slug: string,
|
||||
): Promise<RuleTemplateDto | null> {
|
||||
if (!isDatabaseConfigured()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return await prisma.ruleTemplate.findUnique({
|
||||
where: { slug },
|
||||
select: TEMPLATE_SELECT,
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export type TemplateScore = {
|
||||
score: number;
|
||||
matchedFacets: string[];
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import { METHOD_FACET_API_SECTION_IDS } from "../../create/customRuleFacets";
|
||||
import {
|
||||
CATALOG_SECTION_IDS,
|
||||
METHOD_FACET_API_SECTION_IDS,
|
||||
} from "../../create/customRuleFacets";
|
||||
|
||||
/**
|
||||
* Zod schemas for the recommendation matrix (CR-88).
|
||||
@@ -20,6 +23,10 @@ export const SECTION_IDS = METHOD_FACET_API_SECTION_IDS;
|
||||
export type SectionId = (typeof SECTION_IDS)[number];
|
||||
export const sectionIdSchema = z.enum(SECTION_IDS);
|
||||
|
||||
/** `GET /api/create-flow/methods?section=` including core values (CR-115). */
|
||||
export type CatalogSectionId = (typeof CATALOG_SECTION_IDS)[number];
|
||||
export const catalogSectionIdSchema = z.enum(CATALOG_SECTION_IDS);
|
||||
|
||||
export const FACET_GROUP_IDS = [
|
||||
"size",
|
||||
"orgType",
|
||||
|
||||
Reference in New Issue
Block a user