@ogygie/studio 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.js
CHANGED
|
@@ -826,6 +826,15 @@ function compositionOfSpec(content) {
|
|
|
826
826
|
const section = next === null ? rest : rest.slice(0, next.index);
|
|
827
827
|
return COMPOSITION_LINE.exec(section)?.[1] ?? null;
|
|
828
828
|
}
|
|
829
|
+
var ASSUMPTIONS_SECTION_TITLE = "Hypoth\xE8ses";
|
|
830
|
+
function assumptionsOfSpec(content) {
|
|
831
|
+
const heading = new RegExp(`^# ${ASSUMPTIONS_SECTION_TITLE}\\s*$`, "mu").exec(content);
|
|
832
|
+
if (heading === null) return [];
|
|
833
|
+
const rest = content.slice(heading.index + heading[0].length);
|
|
834
|
+
const next = /^# /mu.exec(rest);
|
|
835
|
+
const section = next === null ? rest : rest.slice(0, next.index);
|
|
836
|
+
return section.split("\n").map((line) => line.trim()).filter((line) => /^[-*]\s+\S/u.test(line)).map((line) => line.replace(/^[-*]\s+/u, "").trim());
|
|
837
|
+
}
|
|
829
838
|
|
|
830
839
|
// ../ogygie-core/src/proposal.ts
|
|
831
840
|
var DECISION_FILES = ["DECISIONS.md", "DECISIONS.product.md"];
|
|
@@ -1326,6 +1335,16 @@ function parseSpec(filePath, fileName, content) {
|
|
|
1326
1335
|
}
|
|
1327
1336
|
return { ok: true, spec: { frontmatter: checked.data, body: split.body } };
|
|
1328
1337
|
}
|
|
1338
|
+
function intentOf(body) {
|
|
1339
|
+
const heading = /^# @intent\s*$/mu.exec(body);
|
|
1340
|
+
if (heading === null) return "";
|
|
1341
|
+
const rest = body.slice(heading.index + heading[0].length);
|
|
1342
|
+
const next = /^# /mu.exec(rest);
|
|
1343
|
+
const section = (next === null ? rest : rest.slice(0, next.index)).trim();
|
|
1344
|
+
const paragraph = section.split(/\n\s*\n/u)[0]?.replace(/\s+/gu, " ").trim() ?? "";
|
|
1345
|
+
const sentence = /^.*?[.!?](?:\s|$)/u.exec(paragraph)?.[0]?.trim() ?? paragraph;
|
|
1346
|
+
return sentence.length > 160 ? `${sentence.slice(0, 157).trimEnd()}\u2026` : sentence;
|
|
1347
|
+
}
|
|
1329
1348
|
function toReadableSummary(filePath, spec) {
|
|
1330
1349
|
return {
|
|
1331
1350
|
id: spec.frontmatter.id,
|
|
@@ -1333,7 +1352,8 @@ function toReadableSummary(filePath, spec) {
|
|
|
1333
1352
|
priority: spec.frontmatter.priority,
|
|
1334
1353
|
depends_on: spec.frontmatter.depends_on,
|
|
1335
1354
|
filePath,
|
|
1336
|
-
presentSections: presentSectionsOf(spec.body)
|
|
1355
|
+
presentSections: presentSectionsOf(spec.body),
|
|
1356
|
+
intent: intentOf(spec.body)
|
|
1337
1357
|
};
|
|
1338
1358
|
}
|
|
1339
1359
|
async function listSpecFileNames(deps) {
|
|
@@ -1540,7 +1560,9 @@ var ReadableSpecSummarySchema = z6.object({
|
|
|
1540
1560
|
priority: SpecFrontmatterSchema.shape.priority,
|
|
1541
1561
|
depends_on: z6.array(FeatureNameSchema),
|
|
1542
1562
|
filePath: z6.string(),
|
|
1543
|
-
presentSections: z6.array(z6.string())
|
|
1563
|
+
presentSections: z6.array(z6.string()),
|
|
1564
|
+
/** The first sentence of `# @intent`, for a list that reads (v1.20: the target-spec suggestions of the brief). */
|
|
1565
|
+
intent: z6.string().default("")
|
|
1544
1566
|
});
|
|
1545
1567
|
var UnreadableSpecSummarySchema = z6.object({
|
|
1546
1568
|
id: z6.string(),
|
|
@@ -1584,16 +1606,31 @@ var BriefAttachmentSchema = z6.object({
|
|
|
1584
1606
|
name: z6.string().min(1).max(200),
|
|
1585
1607
|
contentBase64: z6.string().min(1).max(57e5)
|
|
1586
1608
|
});
|
|
1609
|
+
var BriefAnswerSchema = z6.object({ questionId: FeatureNameSchema, answer: z6.string().min(1).max(2e3) });
|
|
1587
1610
|
var BriefInputSchema = z6.object({
|
|
1588
1611
|
brief: z6.string().min(50).max(2e4),
|
|
1589
1612
|
/** Absent: the model names the new spec. Present and existing: an amendment. */
|
|
1590
1613
|
specId: FeatureNameSchema.optional(),
|
|
1591
1614
|
/** The client's material — a mock-up, a workbook, a document; five at most, formats md, txt, csv, json, html, xlsx, pdf. */
|
|
1592
|
-
attachments: z6.array(BriefAttachmentSchema).max(5).default([])
|
|
1615
|
+
attachments: z6.array(BriefAttachmentSchema).max(5).default([]),
|
|
1616
|
+
/** D57: the answers of the past rounds of questions, three rounds at most. */
|
|
1617
|
+
answers: z6.array(BriefAnswerSchema).max(15).default([]),
|
|
1618
|
+
/** D57: the round this send opens; at 3 the draft is asked without a question. */
|
|
1619
|
+
round: z6.number().int().min(1).max(4).default(1),
|
|
1620
|
+
/** D57: « Passer les questions » — the draft is asked, the unknown becomes an assumption. */
|
|
1621
|
+
skipQuestions: z6.boolean().default(false)
|
|
1622
|
+
});
|
|
1623
|
+
var BriefQuestionSchema = z6.object({
|
|
1624
|
+
/** kebab-case, stable from one read to the next. */
|
|
1625
|
+
id: FeatureNameSchema,
|
|
1626
|
+
text: z6.string().min(1),
|
|
1627
|
+
/** The proposed answers; a free answer is always possible. */
|
|
1628
|
+
options: z6.array(z6.string().min(1)).min(2).max(4)
|
|
1593
1629
|
});
|
|
1594
1630
|
var BriefOutputSchema = z6.object({
|
|
1595
1631
|
specId: FeatureNameSchema,
|
|
1596
|
-
|
|
1632
|
+
/** 1 when the first draft passed; up to 4 when the model corrected itself and a refused round of questions cost a turn (D57). */
|
|
1633
|
+
attempts: z6.number().int().min(1).max(4),
|
|
1597
1634
|
problemsFixed: z6.array(z6.string()),
|
|
1598
1635
|
/** No feature of the manifest carries this id: its first schemas.ts awaits the operator (D52, lot C). */
|
|
1599
1636
|
isNewFeature: z6.boolean(),
|
|
@@ -1602,10 +1639,30 @@ var BriefOutputSchema = z6.object({
|
|
|
1602
1639
|
compositions: z6.array(z6.string()).default([]),
|
|
1603
1640
|
/** The generator's tally on the draft's `# Génération` block (lines bound without a model, lines carried by a hole); null without a block. */
|
|
1604
1641
|
generation: z6.object({ derivable: z6.number().int().nonnegative(), hole: z6.number().int().nonnegative() }).nullable().default(null),
|
|
1642
|
+
/** D57: the points the model decided that the brief did not say — the `# Hypothèses` lines of the draft. */
|
|
1643
|
+
assumptions: z6.array(z6.string()).default([]),
|
|
1605
1644
|
usage: z6.array(
|
|
1606
1645
|
z6.object({ model: z6.string(), inputTokens: z6.number().int().nonnegative(), outputTokens: z6.number().int().nonnegative() })
|
|
1607
1646
|
)
|
|
1608
1647
|
});
|
|
1648
|
+
var BriefStartOutputSchema = z6.object({ jobId: z6.string() });
|
|
1649
|
+
var BriefStageSchema = z6.enum(["questions", "drafting", "judging", "compositions", "writing", "done", "failed"]);
|
|
1650
|
+
var BriefJobSchema = z6.object({
|
|
1651
|
+
jobId: z6.string(),
|
|
1652
|
+
stage: BriefStageSchema,
|
|
1653
|
+
/** On questions: the round in progress. */
|
|
1654
|
+
round: z6.number().int().min(1).max(3).optional(),
|
|
1655
|
+
/** On drafting / judging: the attempt. */
|
|
1656
|
+
attempt: z6.number().int().min(1).max(4).optional(),
|
|
1657
|
+
/** On compositions: k of N. */
|
|
1658
|
+
composition: z6.object({ done: z6.number().int().nonnegative(), total: z6.number().int().nonnegative() }).optional(),
|
|
1659
|
+
elapsedMs: z6.number().int().nonnegative(),
|
|
1660
|
+
/** The P95 of B3: 90 000 ms. */
|
|
1661
|
+
budgetMs: z6.number().int().positive(),
|
|
1662
|
+
questions: z6.array(BriefQuestionSchema).min(3).max(5).optional(),
|
|
1663
|
+
result: BriefOutputSchema.optional(),
|
|
1664
|
+
error: OgygieErrorSchema.optional()
|
|
1665
|
+
});
|
|
1609
1666
|
var ChecksStateSchema = z6.enum(["green", "red", "pending", "none"]);
|
|
1610
1667
|
var PullRequestStatusSchema = z6.object({
|
|
1611
1668
|
specId: FeatureNameSchema,
|
|
@@ -1648,6 +1705,26 @@ var EnvironmentsStatusSchema = z6.object({
|
|
|
1648
1705
|
/** release/<date>.<n>, most recent first. */
|
|
1649
1706
|
releases: z6.array(z6.string())
|
|
1650
1707
|
});
|
|
1708
|
+
var EnvironmentReadParamsSchema = z6.object({ environment: EnvironmentNameSchema });
|
|
1709
|
+
var CommitReachSchema = z6.enum(["deployed", "forward", "backward", "unreachable"]);
|
|
1710
|
+
var EnvironmentCommitSchema = z6.object({
|
|
1711
|
+
sha: z6.string(),
|
|
1712
|
+
/** The first line of the message without its `spec: merge` prefix and its `(#n)` suffix. */
|
|
1713
|
+
title: z6.string(),
|
|
1714
|
+
specId: FeatureNameSchema.nullable(),
|
|
1715
|
+
author: z6.string(),
|
|
1716
|
+
authoredAt: z6.string(),
|
|
1717
|
+
releaseTag: z6.string().nullable(),
|
|
1718
|
+
reach: CommitReachSchema,
|
|
1719
|
+
/** The reason, in the client's language. */
|
|
1720
|
+
why: z6.string()
|
|
1721
|
+
});
|
|
1722
|
+
var EnvironmentCommitsSchema = z6.object({
|
|
1723
|
+
environment: EnvironmentNameSchema,
|
|
1724
|
+
ref: z6.string(),
|
|
1725
|
+
deployedSha: z6.string().nullable(),
|
|
1726
|
+
commits: z6.array(EnvironmentCommitSchema).max(30)
|
|
1727
|
+
});
|
|
1651
1728
|
var PromotionInputSchema = z6.object({
|
|
1652
1729
|
sha: z6.string().regex(/^[0-9a-f]{40}$/),
|
|
1653
1730
|
/** Operator only: a step back, forced, with its reason. */
|
|
@@ -2031,6 +2108,9 @@ ${link}
|
|
|
2031
2108
|
});
|
|
2032
2109
|
}
|
|
2033
2110
|
|
|
2111
|
+
// src/server/hosted/brief-routes.ts
|
|
2112
|
+
import { z as z10 } from "zod";
|
|
2113
|
+
|
|
2034
2114
|
// src/server/hosted/attachments.ts
|
|
2035
2115
|
import { createHash } from "node:crypto";
|
|
2036
2116
|
import { inflateRawSync } from "node:zlib";
|
|
@@ -2276,6 +2356,9 @@ var MAX_COMPOSITIONS = 3;
|
|
|
2276
2356
|
var COMPOSITION_SEPARATOR = "=== COMPOSITION ===";
|
|
2277
2357
|
var DERIVED_BRIEF_MAX = 6e3;
|
|
2278
2358
|
var BRIEF_IN_MESSAGE_MAX = 2e3;
|
|
2359
|
+
var MAX_QUESTION_ROUNDS = 3;
|
|
2360
|
+
var QUESTIONS_MARKER = /^\s*QUESTIONS\s*$/mu;
|
|
2361
|
+
var NO_MORE_QUESTIONS = "no more questions: write the draft now, and put what you do not know under # Hypoth\xE8ses";
|
|
2279
2362
|
async function readIfPresent(storage, path) {
|
|
2280
2363
|
return await storage.exists(path) ? storage.readFile(path) : null;
|
|
2281
2364
|
}
|
|
@@ -2339,6 +2422,25 @@ async function existingSpecIds(storage) {
|
|
|
2339
2422
|
const entries = await storage.listDirectory("specs");
|
|
2340
2423
|
return entries.map((entry) => entry.path).filter((path) => !path.includes("/") && path.endsWith(".spec.md") && !path.startsWith("_")).map((path) => path.replace(/\.spec\.md$/, "")).sort();
|
|
2341
2424
|
}
|
|
2425
|
+
function extractQuestions(text) {
|
|
2426
|
+
const marker = QUESTIONS_MARKER.exec(text);
|
|
2427
|
+
if (marker === null) return null;
|
|
2428
|
+
const raw = text.slice(marker.index + marker[0].length).replace(/^\s*```(?:json)?\s*/u, "").replace(/\s*```\s*$/u, "").trim();
|
|
2429
|
+
let parsed;
|
|
2430
|
+
try {
|
|
2431
|
+
parsed = JSON.parse(raw);
|
|
2432
|
+
} catch {
|
|
2433
|
+
return { problem: "QUESTIONS: the JSON after the marker does not parse" };
|
|
2434
|
+
}
|
|
2435
|
+
const shape = z9.object({
|
|
2436
|
+
questions: z9.array(z9.object({ id: FeatureNameSchema, text: z9.string().trim().min(1), options: z9.array(z9.string().trim().min(1)).min(2).max(4) })).min(3).max(5)
|
|
2437
|
+
});
|
|
2438
|
+
const checked = shape.safeParse(parsed);
|
|
2439
|
+
if (!checked.success) return { problem: `QUESTIONS: ${checked.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(" ; ")}` };
|
|
2440
|
+
const ids = new Set(checked.data.questions.map((question) => question.id));
|
|
2441
|
+
if (ids.size !== checked.data.questions.length) return { problem: "QUESTIONS: two questions share an id" };
|
|
2442
|
+
return { questions: checked.data.questions };
|
|
2443
|
+
}
|
|
2342
2444
|
function buildBriefPrompt(context) {
|
|
2343
2445
|
const system = [
|
|
2344
2446
|
"Tu r\xE9diges des SPECS de features pour un produit construit avec le framework Ogygie. Une spec est un contrat, relu et sign\xE9 par un humain non d\xE9veloppeur, puis impl\xE9ment\xE9 par une cha\xEEne de g\xE9n\xE9ration : tu \xE9cris pour ce lecteur, en fran\xE7ais, pr\xE9cis, sans jargon.",
|
|
@@ -2358,6 +2460,8 @@ function buildBriefPrompt(context) {
|
|
|
2358
2460
|
"- Tu n'\xE9cris jamais de code ni de design (couleurs, composants) : la spec dit QUOI, jamais COMMENT. Le bloc YAML de `# G\xE9n\xE9ration` n'est pas du code : c'est la d\xE9claration que le g\xE9n\xE9rateur lit.",
|
|
2359
2461
|
...context.grammarCard === null || context.grammarCard === void 0 ? [] : ["- Quand la rubrique apporte des op\xE9rations ou un \xE9cran que le g\xE9n\xE9rateur sait d\xE9river, \xE9cris la section `# G\xE9n\xE9ration` apr\xE8s `# @intent` et ses `# Comportements` dans la grammaire ferm\xE9e (carte ci-dessous) : le brouillon est jug\xE9 par le v\xE9rificateur du g\xE9n\xE9rateur avant d'\xEAtre montr\xE9. Quand elle ne se g\xE9n\xE8re pas, n'\xE9cris pas la section."],
|
|
2360
2462
|
"- Tu ne proposes jamais `status: validated` ni `implemented` : valider est le geste de l'humain.",
|
|
2463
|
+
"- HYPOTH\xC8SES : quand tu tranches un point que le brief ne dit pas (un nombre, un ordre, un p\xE9rim\xE8tre, un mot), ajoute apr\xE8s `# Budget` (et avant `# Design` si elle existe) une section `# Hypoth\xE8ses`, une puce par choix, en fran\xE7ais, courte ; sans hypoth\xE8se, pas de section.",
|
|
2464
|
+
'- QUESTIONS : quand la demande ci-dessous t\'y autorise et qu\'une r\xE9ponse du client CHANGERAIT la spec, tu peux, AU LIEU du fichier, r\xE9pondre par une ligne `QUESTIONS` seule puis un objet JSON `{"questions":[{"id":"kebab-case","text":"la question, en fran\xE7ais","options":["r\xE9ponse","r\xE9ponse"]}]}` \u2014 trois \xE0 cinq questions ferm\xE9es, deux \xE0 quatre options chacune, rien d\'autre avant ni apr\xE8s. Une question dont la r\xE9ponse ne changerait pas la spec ne se pose pas : elle devient une hypoth\xE8se.',
|
|
2361
2465
|
"- Quand la rubrique est un \xC9CRAN (une page du tableau de bord), ajoute apr\xE8s `# Budget` une section `# Design` qui commence par une ligne `Composition : <nom-kebab>` (le nom de la disposition, ex. `liste-d-abord`), puis l'ordre des vues, le pattern du catalogue de chacune (liste, formulaire, \xE9tat vide\u2026) et les \xE9tats mis en avant \u2014 jamais une couleur, jamais un composant invent\xE9.",
|
|
2362
2466
|
"",
|
|
2363
2467
|
"GABARIT DU REPO (\xE0 respecter \xE0 la lettre) :",
|
|
@@ -2379,7 +2483,10 @@ function buildBriefPrompt(context) {
|
|
|
2379
2483
|
...context.existingSpec === null ? [] : ["SPEC EXISTANTE :", context.existingSpec, ""],
|
|
2380
2484
|
"BRIEF :",
|
|
2381
2485
|
context.brief.trim(),
|
|
2382
|
-
...attachmentsPromptSection(context.attachments ?? [])
|
|
2486
|
+
...attachmentsPromptSection(context.attachments ?? []),
|
|
2487
|
+
...(context.answers ?? []).length === 0 ? [] : ["", "R\xC9PONSES DU CLIENT \xC0 TES QUESTIONS :", ...(context.answers ?? []).map((answer) => `- ${answer.questionId} : ${answer.answer}`)],
|
|
2488
|
+
"",
|
|
2489
|
+
context.questions?.allowed === true ? `Tourn\xE9e ${context.questions.round} sur ${MAX_QUESTION_ROUNDS} : si une r\xE9ponse du client changerait la spec, pose tes questions (format QUESTIONS) ; sinon, r\xE9dige le fichier.` : "N'interroge plus : r\xE9dige le fichier maintenant, et consigne ce que tu ne sais pas sous `# Hypoth\xE8ses`."
|
|
2383
2490
|
].join("\n");
|
|
2384
2491
|
return { system, user, documents: attachmentDocumentsOf(context.attachments ?? []) };
|
|
2385
2492
|
}
|
|
@@ -2471,7 +2578,7 @@ function judgeDraft(input) {
|
|
|
2471
2578
|
}
|
|
2472
2579
|
return { specId, problems: [...new Set(problems)] };
|
|
2473
2580
|
}
|
|
2474
|
-
async function
|
|
2581
|
+
async function runBrief(deps, input) {
|
|
2475
2582
|
if (input.specId !== void 0 && !FeatureNameSchema.safeParse(input.specId).success) {
|
|
2476
2583
|
throwOgygieError(
|
|
2477
2584
|
{
|
|
@@ -2502,6 +2609,8 @@ async function draftSpecFromBrief(deps, input) {
|
|
|
2502
2609
|
const knownDecisionIds = (await Promise.all(DECISION_FILES.map((file) => readIfPresent(deps.storage, file)))).flatMap((content) => parseKnownDecisionIds(content ?? ""));
|
|
2503
2610
|
const existingSpec = input.specId !== void 0 ? await readIfPresent(deps.storage, `specs/${input.specId}.spec.md`) : null;
|
|
2504
2611
|
const isAmendment = existingSpec !== null;
|
|
2612
|
+
const round = Math.min(Math.max(input.round ?? 1, 1), MAX_QUESTION_ROUNDS + 1);
|
|
2613
|
+
const questionsAllowed = input.skipQuestions !== true && round <= MAX_QUESTION_ROUNDS;
|
|
2505
2614
|
const prompt = buildBriefPrompt({
|
|
2506
2615
|
template,
|
|
2507
2616
|
productRules,
|
|
@@ -2512,15 +2621,40 @@ async function draftSpecFromBrief(deps, input) {
|
|
|
2512
2621
|
existingSpec,
|
|
2513
2622
|
specId: input.specId,
|
|
2514
2623
|
brief: input.brief,
|
|
2515
|
-
attachments: input.attachments
|
|
2624
|
+
attachments: input.attachments,
|
|
2625
|
+
answers: input.answers,
|
|
2626
|
+
questions: { allowed: questionsAllowed, round }
|
|
2516
2627
|
});
|
|
2517
2628
|
const usage = [];
|
|
2629
|
+
let refusedQuestions = false;
|
|
2518
2630
|
let previous;
|
|
2519
2631
|
let firstProblems = [];
|
|
2520
2632
|
let maxAttempts = MAX_ATTEMPTS;
|
|
2521
2633
|
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
2634
|
+
deps.onStage?.({ stage: "drafting", attempt });
|
|
2522
2635
|
const answer = await deps.writer.write({ ...prompt, previous });
|
|
2523
2636
|
usage.push({ model: answer.model, ...answer.usage });
|
|
2637
|
+
const asked = extractQuestions(answer.text);
|
|
2638
|
+
if (asked !== null) {
|
|
2639
|
+
if ("problem" in asked) {
|
|
2640
|
+
deps.log(JSON.stringify({ action: "brief", attempt, questions: "malformed", ...answer.usage }));
|
|
2641
|
+
if (attempt === 1) firstProblems = [asked.problem];
|
|
2642
|
+
previous = { draft: answer.text, problems: [asked.problem] };
|
|
2643
|
+
continue;
|
|
2644
|
+
}
|
|
2645
|
+
if (questionsAllowed) {
|
|
2646
|
+
deps.log(JSON.stringify({ action: "brief", attempt, round, questions: asked.questions.length, ...answer.usage }));
|
|
2647
|
+
return { kind: "questions", round, questions: asked.questions, usage };
|
|
2648
|
+
}
|
|
2649
|
+
deps.log(JSON.stringify({ action: "brief", attempt, round, questions: "refused", ...answer.usage }));
|
|
2650
|
+
if (!refusedQuestions) {
|
|
2651
|
+
refusedQuestions = true;
|
|
2652
|
+
maxAttempts += 1;
|
|
2653
|
+
}
|
|
2654
|
+
previous = { draft: answer.text, problems: [NO_MORE_QUESTIONS] };
|
|
2655
|
+
continue;
|
|
2656
|
+
}
|
|
2657
|
+
deps.onStage?.({ stage: "judging", attempt });
|
|
2524
2658
|
const content = extractSpecText(answer.text);
|
|
2525
2659
|
const judged = judgeDraft({
|
|
2526
2660
|
content,
|
|
@@ -2535,7 +2669,11 @@ async function draftSpecFromBrief(deps, input) {
|
|
|
2535
2669
|
deps.log(JSON.stringify({ action: "brief", attempt, specId: judged.specId, problems: judged.problems.length, generated: generationJudgement.tally !== null, ...answer.usage }));
|
|
2536
2670
|
if (judged.problems.length === 0 && judged.specId !== null) {
|
|
2537
2671
|
const specId = judged.specId;
|
|
2538
|
-
const
|
|
2672
|
+
const assumptions = assumptionsOfSpec(content);
|
|
2673
|
+
const briefForMessage = (input.brief.length > BRIEF_IN_MESSAGE_MAX ? `${input.brief.slice(0, BRIEF_IN_MESSAGE_MAX)} (\u2026)` : input.brief) + attachmentsProvenance(input.attachments ?? []) + (assumptions.length === 0 ? "" : `
|
|
2674
|
+
|
|
2675
|
+
Hypoth\xE8ses du mod\xE8le (D57) :
|
|
2676
|
+
${assumptions.map((line) => `- ${line}`).join("\n")}`);
|
|
2539
2677
|
const composition = isAmendment ? null : compositionOfSpec(content);
|
|
2540
2678
|
const compositions = [];
|
|
2541
2679
|
if (composition !== null) {
|
|
@@ -2553,7 +2691,9 @@ async function draftSpecFromBrief(deps, input) {
|
|
|
2553
2691
|
deps.log(JSON.stringify({ action: "composition", specId, outcome: thrown instanceof OgygieFailure ? thrown.error.code : "error" }));
|
|
2554
2692
|
}
|
|
2555
2693
|
const seen = /* @__PURE__ */ new Set([composition]);
|
|
2556
|
-
|
|
2694
|
+
const candidates = splitCompositions(answerText);
|
|
2695
|
+
deps.onStage?.({ stage: "compositions", composition: { done: 0, total: Math.min(candidates.length, MAX_COMPOSITIONS - 1) } });
|
|
2696
|
+
for (const candidate of candidates) {
|
|
2557
2697
|
if (seen.size >= MAX_COMPOSITIONS) break;
|
|
2558
2698
|
const name = compositionOfSpec(candidate);
|
|
2559
2699
|
const verdict = judgeDraft({ content: candidate, expectedId: specId, knownDecisionIds, takenIds: specIds, isAmendment: false });
|
|
@@ -2561,6 +2701,7 @@ async function draftSpecFromBrief(deps, input) {
|
|
|
2561
2701
|
deps.log(JSON.stringify({ action: "composition", specId, composition: name, problems: problems.length }));
|
|
2562
2702
|
if (problems.length > 0 || name === null) continue;
|
|
2563
2703
|
seen.add(name);
|
|
2704
|
+
deps.onStage?.({ stage: "compositions", composition: { done: seen.size - 1, total: Math.min(candidates.length, MAX_COMPOSITIONS - 1) } });
|
|
2564
2705
|
await deps.storage.write(`specs/${specId}.spec.md`, candidate, `spec: composition ${name} pour ${specId}, propos\xE9e par le mod\xE8le
|
|
2565
2706
|
|
|
2566
2707
|
Une autre disposition du m\xEAme \xE9cran ; la composition retenue par d\xE9faut suit dans le commit suivant.
|
|
@@ -2571,6 +2712,7 @@ ${briefForMessage}`);
|
|
|
2571
2712
|
}
|
|
2572
2713
|
compositions.unshift(composition);
|
|
2573
2714
|
}
|
|
2715
|
+
deps.onStage?.({ stage: "writing" });
|
|
2574
2716
|
await deps.storage.write(
|
|
2575
2717
|
`specs/${specId}.spec.md`,
|
|
2576
2718
|
content,
|
|
@@ -2580,14 +2722,18 @@ Brief :
|
|
|
2580
2722
|
${briefForMessage}`
|
|
2581
2723
|
);
|
|
2582
2724
|
return {
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2725
|
+
kind: "draft",
|
|
2726
|
+
result: {
|
|
2727
|
+
specId,
|
|
2728
|
+
attempts: attempt,
|
|
2729
|
+
problemsFixed: firstProblems,
|
|
2730
|
+
isNewFeature: !manifest.featureIds.has(specId),
|
|
2731
|
+
isAmendment,
|
|
2732
|
+
compositions,
|
|
2733
|
+
generation: generationJudgement.tally,
|
|
2734
|
+
assumptions,
|
|
2735
|
+
usage
|
|
2736
|
+
}
|
|
2591
2737
|
};
|
|
2592
2738
|
}
|
|
2593
2739
|
if (attempt === 1) firstProblems = judged.problems;
|
|
@@ -2606,37 +2752,144 @@ ${briefForMessage}`
|
|
|
2606
2752
|
);
|
|
2607
2753
|
}
|
|
2608
2754
|
|
|
2755
|
+
// src/server/hosted/brief-jobs.ts
|
|
2756
|
+
import { randomUUID } from "node:crypto";
|
|
2757
|
+
var BRIEF_BUDGET_MS = 9e4;
|
|
2758
|
+
var BRIEF_JOB_TTL_MS = 15 * 6e4;
|
|
2759
|
+
function createBriefJobs(deps) {
|
|
2760
|
+
const budgetMs = deps.budgetMs ?? BRIEF_BUDGET_MS;
|
|
2761
|
+
const ttlMs = deps.ttlMs ?? BRIEF_JOB_TTL_MS;
|
|
2762
|
+
const idOf = deps.idOf ?? randomUUID;
|
|
2763
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
2764
|
+
const settlements = /* @__PURE__ */ new Map();
|
|
2765
|
+
const sweep = () => {
|
|
2766
|
+
const now = deps.now();
|
|
2767
|
+
for (const [jobId, job] of jobs) {
|
|
2768
|
+
if (job.endedAt !== null && job.endedAt + ttlMs <= now) {
|
|
2769
|
+
jobs.delete(jobId);
|
|
2770
|
+
settlements.delete(jobId);
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
};
|
|
2774
|
+
const errorOf = (thrown) => thrown instanceof OgygieFailure ? thrown.error : ogygieError({
|
|
2775
|
+
code: "BRIEF_FAILED",
|
|
2776
|
+
message: "The brief failed before a draft or a round of questions came out.",
|
|
2777
|
+
file: null,
|
|
2778
|
+
cause: thrown instanceof Error ? thrown.message : String(thrown),
|
|
2779
|
+
suggestedFix: "Send the brief again; if it repeats, read the instance log."
|
|
2780
|
+
});
|
|
2781
|
+
return {
|
|
2782
|
+
start(owner, run) {
|
|
2783
|
+
sweep();
|
|
2784
|
+
const running = [...jobs.values()].find((job2) => job2.owner === owner && job2.endedAt === null);
|
|
2785
|
+
if (running !== void 0) {
|
|
2786
|
+
throwOgygieError(
|
|
2787
|
+
{
|
|
2788
|
+
code: "BRIEF_IN_PROGRESS",
|
|
2789
|
+
message: "A brief of yours is still running: one at a time.",
|
|
2790
|
+
file: null,
|
|
2791
|
+
cause: `job ${running.jobId} started ${deps.now() - running.startedAt} ms ago`,
|
|
2792
|
+
suggestedFix: "Wait for it to end (GET /api/briefs/<id>), then send the next brief.",
|
|
2793
|
+
details: { jobId: running.jobId }
|
|
2794
|
+
},
|
|
2795
|
+
409
|
|
2796
|
+
);
|
|
2797
|
+
}
|
|
2798
|
+
const job = { jobId: idOf(), owner, startedAt: deps.now(), endedAt: null, progress: null, outcome: null, error: null };
|
|
2799
|
+
jobs.set(job.jobId, job);
|
|
2800
|
+
const settlement = run((progress) => {
|
|
2801
|
+
job.progress = progress;
|
|
2802
|
+
}).then((outcome) => {
|
|
2803
|
+
job.outcome = outcome;
|
|
2804
|
+
}).catch((thrown) => {
|
|
2805
|
+
job.error = errorOf(thrown);
|
|
2806
|
+
}).finally(() => {
|
|
2807
|
+
job.endedAt = deps.now();
|
|
2808
|
+
});
|
|
2809
|
+
settlements.set(job.jobId, settlement);
|
|
2810
|
+
return job.jobId;
|
|
2811
|
+
},
|
|
2812
|
+
read(jobId) {
|
|
2813
|
+
sweep();
|
|
2814
|
+
const job = jobs.get(jobId);
|
|
2815
|
+
if (job === void 0) {
|
|
2816
|
+
return throwOgygieError(
|
|
2817
|
+
{
|
|
2818
|
+
code: "BRIEF_JOB_LOST",
|
|
2819
|
+
message: "This brief job is unknown here: it expired, or the instance restarted since.",
|
|
2820
|
+
file: null,
|
|
2821
|
+
cause: `no job ${jobId} in memory`,
|
|
2822
|
+
suggestedFix: "Send the brief again; its text is still in your editor."
|
|
2823
|
+
},
|
|
2824
|
+
404
|
|
2825
|
+
);
|
|
2826
|
+
}
|
|
2827
|
+
const base = { jobId: job.jobId, elapsedMs: Math.max(0, (job.endedAt ?? deps.now()) - job.startedAt), budgetMs };
|
|
2828
|
+
if (job.error !== null) return { ...base, stage: "failed", error: job.error };
|
|
2829
|
+
if (job.outcome !== null) {
|
|
2830
|
+
return job.outcome.kind === "draft" ? { ...base, stage: "done", result: job.outcome.result } : { ...base, stage: "questions", round: job.outcome.round, questions: job.outcome.questions };
|
|
2831
|
+
}
|
|
2832
|
+
const progress = job.progress ?? { stage: "drafting", attempt: 1 };
|
|
2833
|
+
switch (progress.stage) {
|
|
2834
|
+
case "drafting":
|
|
2835
|
+
case "judging":
|
|
2836
|
+
return { ...base, stage: progress.stage, attempt: progress.attempt };
|
|
2837
|
+
case "compositions":
|
|
2838
|
+
return { ...base, stage: "compositions", composition: progress.composition };
|
|
2839
|
+
case "writing":
|
|
2840
|
+
return { ...base, stage: "writing" };
|
|
2841
|
+
}
|
|
2842
|
+
},
|
|
2843
|
+
async settled(jobId) {
|
|
2844
|
+
await (settlements.get(jobId) ?? Promise.resolve());
|
|
2845
|
+
}
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
|
|
2609
2849
|
// src/server/hosted/brief-routes.ts
|
|
2610
2850
|
var BRIEF_BODY_LIMIT = 16 * 1024 * 1024;
|
|
2611
2851
|
async function registerBriefRoutes(app, deps) {
|
|
2612
2852
|
const route = app.withTypeProvider();
|
|
2853
|
+
const now = deps.now ?? Date.now;
|
|
2854
|
+
const jobs = deps.jobs ?? createBriefJobs({ now });
|
|
2613
2855
|
route.route({
|
|
2614
2856
|
method: "POST",
|
|
2615
2857
|
url: "/api/briefs",
|
|
2616
2858
|
bodyLimit: BRIEF_BODY_LIMIT,
|
|
2617
|
-
schema: { body: BriefInputSchema, response: {
|
|
2859
|
+
schema: { body: BriefInputSchema, response: { 202: BriefStartOutputSchema } },
|
|
2618
2860
|
handler: async (request, reply) => {
|
|
2619
2861
|
const user = hostedUserOf(request);
|
|
2620
2862
|
requireRole(user, "author", "write a brief");
|
|
2621
2863
|
const attachments = prepareBriefAttachments(request.body.attachments ?? []);
|
|
2622
2864
|
const storage = deps.storageFor(user);
|
|
2623
|
-
const
|
|
2624
|
-
|
|
2625
|
-
{
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2865
|
+
const { brief, specId, answers, round, skipQuestions } = request.body;
|
|
2866
|
+
const jobId = jobs.start(user.email, async (onStage) => {
|
|
2867
|
+
const outcome = await runBrief({ storage, writer: deps.writer, log: deps.log, onStage }, { brief, specId, attachments, answers, round, skipQuestions });
|
|
2868
|
+
if (outcome.kind !== "draft") return outcome;
|
|
2869
|
+
const proposal = "lastProposal" in storage ? storage.lastProposal : null;
|
|
2870
|
+
if (deps.usage !== void 0 && proposal !== null) {
|
|
2871
|
+
const at = new Date(now()).toISOString();
|
|
2872
|
+
try {
|
|
2873
|
+
await deps.usage.appendUsage(
|
|
2874
|
+
proposal.pullRequestNumber,
|
|
2875
|
+
outcome.result.usage.map((usage, index) => ({ at, action: "brief", attempt: index + 1, model: usage.model, inputTokens: usage.inputTokens, outputTokens: usage.outputTokens, actor: user.email }))
|
|
2876
|
+
);
|
|
2877
|
+
} catch (thrown) {
|
|
2878
|
+
deps.log(JSON.stringify({ action: "usage-journal", specId: outcome.result.specId, outcome: thrown instanceof Error ? thrown.message : String(thrown) }));
|
|
2879
|
+
}
|
|
2637
2880
|
}
|
|
2638
|
-
|
|
2639
|
-
|
|
2881
|
+
return outcome;
|
|
2882
|
+
});
|
|
2883
|
+
return reply.code(202).send({ jobId });
|
|
2884
|
+
}
|
|
2885
|
+
});
|
|
2886
|
+
route.route({
|
|
2887
|
+
method: "GET",
|
|
2888
|
+
url: "/api/briefs/:jobId",
|
|
2889
|
+
schema: { params: z10.object({ jobId: z10.string().min(1) }), response: { 200: BriefJobSchema } },
|
|
2890
|
+
handler: async (request, reply) => {
|
|
2891
|
+
hostedUserOf(request);
|
|
2892
|
+
return reply.code(200).send(jobs.read(request.params.jobId));
|
|
2640
2893
|
}
|
|
2641
2894
|
});
|
|
2642
2895
|
}
|
|
@@ -3004,6 +3257,48 @@ async function environmentsStatus(deps) {
|
|
|
3004
3257
|
releases: releases.sort().reverse()
|
|
3005
3258
|
};
|
|
3006
3259
|
}
|
|
3260
|
+
var LINE_LENGTH = 30;
|
|
3261
|
+
function commitTitleOf(message) {
|
|
3262
|
+
const firstLine = message.split("\n")[0] ?? "";
|
|
3263
|
+
return firstLine.replace(/^spec: merge /u, "").replace(/\s*\(#\d+\)\s*$/u, "").trim();
|
|
3264
|
+
}
|
|
3265
|
+
function reachOf(environment, index, deployedIndex, stagingIndex) {
|
|
3266
|
+
if (index === deployedIndex) return { reach: "deployed", why: "d\xE9ploy\xE9" };
|
|
3267
|
+
if (environment === "development") return { reach: "unreachable", why: "le d\xE9veloppement suit la branche par d\xE9faut, il ne se promeut pas" };
|
|
3268
|
+
const newer = deployedIndex === -1 || index < deployedIndex;
|
|
3269
|
+
if (environment === "staging") {
|
|
3270
|
+
return newer ? { reach: "forward", why: "avance rapide depuis le d\xE9veloppement" } : { reach: "backward", why: "retour arri\xE8re : op\xE9rateur, avec un motif" };
|
|
3271
|
+
}
|
|
3272
|
+
if (index === stagingIndex && newer) return { reach: "forward", why: "le commit de la pr\xE9production" };
|
|
3273
|
+
if (newer) return { reach: "unreachable", why: stagingIndex !== -1 && index < stagingIndex ? "la pr\xE9production ne l'a pas re\xE7u (PROMOTION_ORDER_VIOLATED)" : "seul le commit de la pr\xE9production est promouvable" };
|
|
3274
|
+
return { reach: "backward", why: "retour arri\xE8re : op\xE9rateur, avec un motif" };
|
|
3275
|
+
}
|
|
3276
|
+
async function environmentCommits(deps, environment) {
|
|
3277
|
+
const ref = environment === "development" ? deps.defaultBranch : ENVIRONMENT_REFS[environment];
|
|
3278
|
+
const [deployedSha, stagingSha, commits, tags] = await Promise.all([
|
|
3279
|
+
deps.deploy.getRefSha(ref),
|
|
3280
|
+
environment === "production" ? deps.deploy.getRefSha(ENVIRONMENT_REFS.staging) : Promise.resolve(null),
|
|
3281
|
+
deps.deploy.listCommits(deps.defaultBranch, LINE_LENGTH),
|
|
3282
|
+
deps.deploy.listReleaseTags("release/")
|
|
3283
|
+
]);
|
|
3284
|
+
const tagOf = new Map(tags.map((tag) => [tag.sha, tag.name]));
|
|
3285
|
+
const deployedIndex = commits.findIndex((commit) => commit.sha === deployedSha);
|
|
3286
|
+
const stagingIndex = stagingSha === null ? -1 : commits.findIndex((commit) => commit.sha === stagingSha);
|
|
3287
|
+
return {
|
|
3288
|
+
environment,
|
|
3289
|
+
ref,
|
|
3290
|
+
deployedSha,
|
|
3291
|
+
commits: commits.map((commit, index) => ({
|
|
3292
|
+
sha: commit.sha,
|
|
3293
|
+
title: commitTitleOf(commit.message),
|
|
3294
|
+
specId: MERGED_SPEC.exec(commit.message.split("\n")[0] ?? "")?.[1] ?? null,
|
|
3295
|
+
author: commit.author,
|
|
3296
|
+
authoredAt: commit.authoredAt,
|
|
3297
|
+
releaseTag: tagOf.get(commit.sha) ?? null,
|
|
3298
|
+
...reachOf(environment, index, deployedIndex, stagingIndex)
|
|
3299
|
+
}))
|
|
3300
|
+
};
|
|
3301
|
+
}
|
|
3007
3302
|
function pullRequestsOf(commits) {
|
|
3008
3303
|
const included = [];
|
|
3009
3304
|
for (const commit of commits) {
|
|
@@ -3134,6 +3429,15 @@ async function registerPromotionRoutes(app, deps) {
|
|
|
3134
3429
|
return reply.code(200).send(await environmentsStatus(promotionDeps));
|
|
3135
3430
|
}
|
|
3136
3431
|
});
|
|
3432
|
+
route.route({
|
|
3433
|
+
method: "GET",
|
|
3434
|
+
url: "/api/environments/:environment/commits",
|
|
3435
|
+
schema: { params: EnvironmentReadParamsSchema, response: { 200: EnvironmentCommitsSchema } },
|
|
3436
|
+
handler: async (request, reply) => {
|
|
3437
|
+
hostedUserOf(request);
|
|
3438
|
+
return reply.code(200).send(await environmentCommits(promotionDeps, request.params.environment));
|
|
3439
|
+
}
|
|
3440
|
+
});
|
|
3137
3441
|
route.route({
|
|
3138
3442
|
method: "POST",
|
|
3139
3443
|
url: "/api/environments/:environment/promotions",
|
|
@@ -3147,17 +3451,17 @@ async function registerPromotionRoutes(app, deps) {
|
|
|
3147
3451
|
}
|
|
3148
3452
|
|
|
3149
3453
|
// src/server/hosted/usage.ts
|
|
3150
|
-
import { z as
|
|
3151
|
-
var UsageEntrySchema =
|
|
3152
|
-
at:
|
|
3454
|
+
import { z as z11 } from "zod";
|
|
3455
|
+
var UsageEntrySchema = z11.object({
|
|
3456
|
+
at: z11.string(),
|
|
3153
3457
|
/** brief → spec from the instance; `speculate` entries arrive with the generator's lot. */
|
|
3154
|
-
action:
|
|
3155
|
-
attempt:
|
|
3156
|
-
model:
|
|
3157
|
-
inputTokens:
|
|
3158
|
-
outputTokens:
|
|
3159
|
-
actor:
|
|
3160
|
-
specId:
|
|
3458
|
+
action: z11.enum(["brief", "speculate"]),
|
|
3459
|
+
attempt: z11.number().int().positive().optional(),
|
|
3460
|
+
model: z11.string(),
|
|
3461
|
+
inputTokens: z11.number().int().nonnegative(),
|
|
3462
|
+
outputTokens: z11.number().int().nonnegative(),
|
|
3463
|
+
actor: z11.string().optional(),
|
|
3464
|
+
specId: z11.string().optional()
|
|
3161
3465
|
});
|
|
3162
3466
|
var USAGE_MARKER = "<!-- ogygie-usage -->";
|
|
3163
3467
|
var DEFAULT_MODEL_COSTS = {
|
|
@@ -3192,7 +3496,7 @@ function parseUsageComment(body) {
|
|
|
3192
3496
|
const block = /```json\n([\s\S]*?)\n```/.exec(body)?.[1];
|
|
3193
3497
|
if (block === void 0) return [];
|
|
3194
3498
|
try {
|
|
3195
|
-
const parsed =
|
|
3499
|
+
const parsed = z11.array(UsageEntrySchema).safeParse(JSON.parse(block));
|
|
3196
3500
|
return parsed.success ? parsed.data : [];
|
|
3197
3501
|
} catch {
|
|
3198
3502
|
return [];
|
|
@@ -3267,9 +3571,9 @@ async function registerConsumptionRoutes(app, deps) {
|
|
|
3267
3571
|
}
|
|
3268
3572
|
|
|
3269
3573
|
// src/server/hosted/preview-routes.ts
|
|
3270
|
-
import { z as
|
|
3271
|
-
var PreviewParamsSchema = SpecParamsSchema.extend({ state:
|
|
3272
|
-
var CompositionPreviewParamsSchema = PreviewParamsSchema.extend({ composition:
|
|
3574
|
+
import { z as z12 } from "zod";
|
|
3575
|
+
var PreviewParamsSchema = SpecParamsSchema.extend({ state: z12.string().min(1).max(80) });
|
|
3576
|
+
var CompositionPreviewParamsSchema = PreviewParamsSchema.extend({ composition: z12.string().min(1).max(80) });
|
|
3273
3577
|
var PREVIEW_CSP = "sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:";
|
|
3274
3578
|
async function registerPreviewRoutes(app, deps) {
|
|
3275
3579
|
const route = app.withTypeProvider();
|
|
@@ -3311,7 +3615,7 @@ async function registerPreviewRoutes(app, deps) {
|
|
|
3311
3615
|
}
|
|
3312
3616
|
|
|
3313
3617
|
// src/server/hosted/composition-routes.ts
|
|
3314
|
-
import { z as
|
|
3618
|
+
import { z as z13 } from "zod";
|
|
3315
3619
|
|
|
3316
3620
|
// src/server/hosted/compositions.ts
|
|
3317
3621
|
var COMPOSITION_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
@@ -3355,7 +3659,7 @@ La composition \xAB ${composition} \xBB est retenue parmi celles propos\xE9es ;
|
|
|
3355
3659
|
}
|
|
3356
3660
|
|
|
3357
3661
|
// src/server/hosted/composition-routes.ts
|
|
3358
|
-
var CompositionParamsSchema = SpecParamsSchema.extend({ composition:
|
|
3662
|
+
var CompositionParamsSchema = SpecParamsSchema.extend({ composition: z13.string().min(1).max(80) });
|
|
3359
3663
|
async function registerCompositionRoutes(app, deps) {
|
|
3360
3664
|
const route = app.withTypeProvider();
|
|
3361
3665
|
route.route({
|
|
@@ -3849,6 +4153,29 @@ function createGithubDeployClient(deps) {
|
|
|
3849
4153
|
commits: (answer?.commits ?? []).map((entry) => ({ sha: entry.sha, message: entry.commit.message, authoredAt: entry.commit.author.date }))
|
|
3850
4154
|
};
|
|
3851
4155
|
},
|
|
4156
|
+
async listReleaseTags(prefix) {
|
|
4157
|
+
const refs = await request("GET", `${repoUrl}/git/matching-refs/tags/${prefix}`, void 0) ?? [];
|
|
4158
|
+
const tags = [];
|
|
4159
|
+
for (const entry of refs) {
|
|
4160
|
+
const name = entry.ref.replace(/^refs\/tags\//u, "");
|
|
4161
|
+
if (entry.object.type !== "tag") {
|
|
4162
|
+
tags.push({ name, sha: entry.object.sha });
|
|
4163
|
+
continue;
|
|
4164
|
+
}
|
|
4165
|
+
const tag = await request("GET", `${repoUrl}/git/tags/${entry.object.sha}`, void 0, true);
|
|
4166
|
+
if (tag !== null) tags.push({ name, sha: tag.object.sha });
|
|
4167
|
+
}
|
|
4168
|
+
return tags;
|
|
4169
|
+
},
|
|
4170
|
+
async listCommits(ref, perPage) {
|
|
4171
|
+
const answer = await request(
|
|
4172
|
+
"GET",
|
|
4173
|
+
`${repoUrl}/commits?sha=${encodeURIComponent(ref)}&per_page=${Math.min(Math.max(perPage, 1), 100)}`,
|
|
4174
|
+
void 0,
|
|
4175
|
+
true
|
|
4176
|
+
);
|
|
4177
|
+
return (answer ?? []).map((entry) => ({ sha: entry.sha, message: entry.commit.message, authoredAt: entry.commit.author.date, author: entry.commit.author.name }));
|
|
4178
|
+
},
|
|
3852
4179
|
async updateRef(ref, sha, force) {
|
|
3853
4180
|
await request("PATCH", `${repoUrl}/git/refs/heads/${encodeURIComponent(ref)}`, { sha, force });
|
|
3854
4181
|
},
|