@ogygie/studio 0.27.0 → 0.29.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
|
@@ -558,14 +558,14 @@ var OgygieManifestSchema = z3.object({
|
|
|
558
558
|
}
|
|
559
559
|
packageNames.add(packageEntry.name);
|
|
560
560
|
});
|
|
561
|
-
const
|
|
561
|
+
const specIdOf2 = (specPath) => (specPath.split("/").pop() ?? specPath).replace(/\.spec\.md$/, "");
|
|
562
562
|
const ownedSpecIds = /* @__PURE__ */ new Set([
|
|
563
563
|
...manifest.features.map((feature) => feature.name),
|
|
564
564
|
...packageNames
|
|
565
565
|
]);
|
|
566
566
|
(manifest.framework?.packages ?? []).forEach((packageEntry, index) => {
|
|
567
567
|
if (packageEntry.spec === null) return;
|
|
568
|
-
const specId =
|
|
568
|
+
const specId = specIdOf2(packageEntry.spec);
|
|
569
569
|
if (ownedSpecIds.has(specId)) {
|
|
570
570
|
context.addIssue({
|
|
571
571
|
code: "custom",
|
|
@@ -1552,211 +1552,587 @@ async function getManifest(deps) {
|
|
|
1552
1552
|
}
|
|
1553
1553
|
|
|
1554
1554
|
// src/server/schemas.ts
|
|
1555
|
+
import { z as z7 } from "zod";
|
|
1556
|
+
|
|
1557
|
+
// ../ogygie-core/src/impact.ts
|
|
1555
1558
|
import { z as z6 } from "zod";
|
|
1559
|
+
|
|
1560
|
+
// ../ogygie-core/src/manifest-owners.ts
|
|
1561
|
+
function specIdOf(specPath) {
|
|
1562
|
+
return (specPath.split("/").pop() ?? specPath).replace(/\.spec\.md$/, "");
|
|
1563
|
+
}
|
|
1564
|
+
function declaredEntries(manifest) {
|
|
1565
|
+
const entries = [];
|
|
1566
|
+
for (const feature of manifest.features) {
|
|
1567
|
+
const owningApp = feature.app === void 0 ? manifest.apps.find((app) => app.type === "api") : manifest.apps.find((app) => app.name === feature.app);
|
|
1568
|
+
entries.push({
|
|
1569
|
+
kind: "feature",
|
|
1570
|
+
name: feature.name,
|
|
1571
|
+
status: feature.status,
|
|
1572
|
+
folder: owningApp === void 0 ? null : `${owningApp.path}/${feature.folder}`,
|
|
1573
|
+
specId: feature.name,
|
|
1574
|
+
appType: owningApp?.type ?? null
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
for (const productPackage of manifest.packages ?? []) {
|
|
1578
|
+
entries.push({
|
|
1579
|
+
kind: "package",
|
|
1580
|
+
name: productPackage.name,
|
|
1581
|
+
status: productPackage.status,
|
|
1582
|
+
folder: productPackage.path,
|
|
1583
|
+
specId: productPackage.name,
|
|
1584
|
+
appType: null
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1587
|
+
for (const frameworkPackage of manifest.framework?.packages ?? []) {
|
|
1588
|
+
entries.push({
|
|
1589
|
+
kind: "framework-package",
|
|
1590
|
+
name: frameworkPackage.name,
|
|
1591
|
+
status: frameworkPackage.status,
|
|
1592
|
+
folder: frameworkPackage.path,
|
|
1593
|
+
// A published npm name is not a spec id: the link is DECLARED.
|
|
1594
|
+
specId: frameworkPackage.spec === null ? null : specIdOf(frameworkPackage.spec),
|
|
1595
|
+
appType: null
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
return entries;
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
// ../ogygie-core/src/module-purity.ts
|
|
1602
|
+
var IMPORT_STATEMENT = /\bimport\b(?![.(])([\s\S]*?)from\s*["']([^"']+)["']|\bimport\s+["']([^"']+)["']/g;
|
|
1603
|
+
var DYNAMIC_LOAD = /\b(?:import\s*\(|require\s*\()\s*["']([^"']+)["']/g;
|
|
1604
|
+
var REEXPORT_STATEMENT = /\bexport\s+(type\s+)?(\*(?:\s+as\s+[A-Za-z_$][\w$]*)?|\{[^}]*\})\s*from\s*["']([^"']+)["']/g;
|
|
1605
|
+
function parseModuleImports(source) {
|
|
1606
|
+
const code = stripComments(source);
|
|
1607
|
+
const imports = [];
|
|
1608
|
+
for (const match of code.matchAll(IMPORT_STATEMENT)) {
|
|
1609
|
+
const [statement, clause, fromSpecifier, bareSpecifier] = match;
|
|
1610
|
+
const specifier = fromSpecifier ?? bareSpecifier;
|
|
1611
|
+
if (specifier === void 0) continue;
|
|
1612
|
+
imports.push({
|
|
1613
|
+
specifier,
|
|
1614
|
+
typeOnly: clause !== void 0 && isTypeOnlyClause(clause),
|
|
1615
|
+
statement: normalizeWhitespace(statement)
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
1618
|
+
for (const match of code.matchAll(REEXPORT_STATEMENT)) {
|
|
1619
|
+
const [statement, typeKeyword, clause, specifier] = match;
|
|
1620
|
+
if (specifier === void 0 || clause === void 0) continue;
|
|
1621
|
+
imports.push({
|
|
1622
|
+
specifier,
|
|
1623
|
+
typeOnly: typeKeyword !== void 0 || isTypeOnlyClause(clause),
|
|
1624
|
+
statement: normalizeWhitespace(statement)
|
|
1625
|
+
});
|
|
1626
|
+
}
|
|
1627
|
+
for (const match of code.matchAll(DYNAMIC_LOAD)) {
|
|
1628
|
+
const [statement, specifier] = match;
|
|
1629
|
+
if (specifier === void 0) continue;
|
|
1630
|
+
imports.push({
|
|
1631
|
+
specifier,
|
|
1632
|
+
typeOnly: false,
|
|
1633
|
+
statement: normalizeWhitespace(statement)
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1636
|
+
return imports;
|
|
1637
|
+
}
|
|
1638
|
+
function isTypeOnlyClause(clause) {
|
|
1639
|
+
const trimmed = clause.trim();
|
|
1640
|
+
if (trimmed.startsWith("type ")) return true;
|
|
1641
|
+
const named = /^\{([\s\S]*)\}$/.exec(trimmed);
|
|
1642
|
+
if (named === null || named[1] === void 0) return false;
|
|
1643
|
+
const bindings = named[1].split(",").map((binding) => binding.trim()).filter((binding) => binding.length > 0);
|
|
1644
|
+
return bindings.length > 0 && bindings.every((binding) => binding.startsWith("type "));
|
|
1645
|
+
}
|
|
1646
|
+
function stripComments(source) {
|
|
1647
|
+
return source.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
|
|
1648
|
+
}
|
|
1649
|
+
function normalizeWhitespace(statement) {
|
|
1650
|
+
return statement.replace(/\s+/g, " ").trim();
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
// ../ogygie-core/src/shared-tables.ts
|
|
1654
|
+
function dbTableNameOf(declaration) {
|
|
1655
|
+
return typeof declaration === "string" ? declaration : declaration.name;
|
|
1656
|
+
}
|
|
1657
|
+
function dbTableNamesOf(feature) {
|
|
1658
|
+
return feature.dbTables.map(dbTableNameOf);
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// ../ogygie-core/src/impact.ts
|
|
1662
|
+
var MapEntryKindSchema = z6.enum(["feature", "package", "framework-package"]);
|
|
1663
|
+
var ImpactScopeSchema = z6.object({
|
|
1664
|
+
scannedFolders: z6.array(z6.string()),
|
|
1665
|
+
coveredKinds: z6.array(MapEntryKindSchema),
|
|
1666
|
+
outOfReach: z6.array(z6.string())
|
|
1667
|
+
});
|
|
1668
|
+
var ImpactReportSchema = z6.object({
|
|
1669
|
+
target: z6.object({
|
|
1670
|
+
name: z6.string(),
|
|
1671
|
+
kind: MapEntryKindSchema,
|
|
1672
|
+
folder: z6.string()
|
|
1673
|
+
}),
|
|
1674
|
+
dependents: z6.array(
|
|
1675
|
+
z6.object({
|
|
1676
|
+
name: z6.string(),
|
|
1677
|
+
kind: MapEntryKindSchema,
|
|
1678
|
+
importedSymbols: z6.array(z6.string()),
|
|
1679
|
+
viaIndexOnly: z6.boolean()
|
|
1680
|
+
})
|
|
1681
|
+
),
|
|
1682
|
+
routes: z6.array(
|
|
1683
|
+
z6.object({ method: z6.string(), path: z6.string(), description: z6.string() })
|
|
1684
|
+
),
|
|
1685
|
+
ownedTables: z6.array(z6.string()),
|
|
1686
|
+
tableReadersOutsideOwner: z6.array(
|
|
1687
|
+
z6.object({ table: z6.string(), feature: z6.string(), file: z6.string() })
|
|
1688
|
+
),
|
|
1689
|
+
findings: z6.array(OgygieErrorSchema),
|
|
1690
|
+
testCommand: z6.string(),
|
|
1691
|
+
scope: ImpactScopeSchema
|
|
1692
|
+
});
|
|
1693
|
+
var SOURCE_EXTENSIONS = [".ts", ".tsx"];
|
|
1694
|
+
function isSource(path) {
|
|
1695
|
+
return SOURCE_EXTENSIONS.some((extension) => path.endsWith(extension));
|
|
1696
|
+
}
|
|
1697
|
+
function isTest(path) {
|
|
1698
|
+
return /\.test\.tsx?$/.test(path);
|
|
1699
|
+
}
|
|
1700
|
+
function symbolsOfImportClause(clause) {
|
|
1701
|
+
const symbols = [];
|
|
1702
|
+
const braces = /\{([^}]*)\}/.exec(clause);
|
|
1703
|
+
if (braces?.[1] !== void 0) {
|
|
1704
|
+
for (const rawEntry of braces[1].split(",")) {
|
|
1705
|
+
const name = rawEntry.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0]?.trim();
|
|
1706
|
+
if (name !== void 0 && name !== "") symbols.push(name);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
const outsideBraces = clause.replace(/\{[^}]*\}/, "").replace(/^type\s+/, "").trim();
|
|
1710
|
+
if (/\*\s*as\s+/.test(outsideBraces)) symbols.push("*");
|
|
1711
|
+
else if (outsideBraces.replace(/,/g, "").trim() !== "") symbols.push("default");
|
|
1712
|
+
return symbols;
|
|
1713
|
+
}
|
|
1714
|
+
function specifiersMatching(target, source) {
|
|
1715
|
+
const folder = target.folder;
|
|
1716
|
+
if (folder === null) return false;
|
|
1717
|
+
const directory = folder.split("/").pop() ?? folder;
|
|
1718
|
+
if (new RegExp(`(^|/)\\.\\./${directory}/`).test(source)) return true;
|
|
1719
|
+
if (source.startsWith(`../${directory}/`) || source === `../${directory}`) return true;
|
|
1720
|
+
if (source === target.name || source.startsWith(`${target.name}/`)) return true;
|
|
1721
|
+
const segments = source.split("/");
|
|
1722
|
+
return segments.includes(directory) && !source.startsWith(".");
|
|
1723
|
+
}
|
|
1724
|
+
function piercesTheIndex(specifier) {
|
|
1725
|
+
if (!specifier.startsWith(".")) return false;
|
|
1726
|
+
return !/(^|\/)index(\.js)?$/.test(specifier);
|
|
1727
|
+
}
|
|
1728
|
+
async function analyzeImpact(deps, targetName) {
|
|
1729
|
+
const entries = declaredEntries(deps.manifest);
|
|
1730
|
+
const target = entries.find((entry) => entry.name === targetName);
|
|
1731
|
+
if (target === void 0) {
|
|
1732
|
+
return throwOgygieError({
|
|
1733
|
+
code: "IMPACT_UNKNOWN_FEATURE",
|
|
1734
|
+
message: `"${targetName}" is unknown to the manifest.`,
|
|
1735
|
+
file: "ogygie.manifest.json",
|
|
1736
|
+
cause: `no entry of the map is named "${targetName}"`,
|
|
1737
|
+
suggestedFix: `Pick one of: ${entries.map((entry) => entry.name).join(", ") || "<the map declares nothing>"}.`
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
return buildReport(deps, entries, target);
|
|
1741
|
+
}
|
|
1742
|
+
async function buildReport(deps, entries, target) {
|
|
1743
|
+
const findings = [];
|
|
1744
|
+
const scannedFolders = [];
|
|
1745
|
+
const outOfReach = [];
|
|
1746
|
+
if (target.folder === null) {
|
|
1747
|
+
outOfReach.push(
|
|
1748
|
+
`the target "${target.name}" has no folder the map can resolve \u2014 its app is not declared`
|
|
1749
|
+
);
|
|
1750
|
+
}
|
|
1751
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
1752
|
+
const tableReadersOutsideOwner = [];
|
|
1753
|
+
const ownedTables = target.kind === "feature" ? tablesOf(deps.manifest, target.name) : [];
|
|
1754
|
+
for (const entry of entries) {
|
|
1755
|
+
if (entry.name === target.name) continue;
|
|
1756
|
+
if (entry.folder === null) {
|
|
1757
|
+
outOfReach.push(
|
|
1758
|
+
`"${entry.name}" (${entry.kind}) \u2014 the map declares no resolvable folder for it`
|
|
1759
|
+
);
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
const files = (await deps.adapter.listDirectory(entry.folder)).filter(
|
|
1763
|
+
(file) => isSource(file.path)
|
|
1764
|
+
);
|
|
1765
|
+
if (files.length > 0) scannedFolders.push(entry.folder);
|
|
1766
|
+
for (const file of files) {
|
|
1767
|
+
const filePath = `${entry.folder}/${file.path}`;
|
|
1768
|
+
const content = await deps.adapter.readFile(filePath);
|
|
1769
|
+
for (const imported of parseModuleImports(content)) {
|
|
1770
|
+
if (!specifiersMatching(target, imported.specifier)) continue;
|
|
1771
|
+
const accumulator = dependents.get(entry.name) ?? {
|
|
1772
|
+
kind: entry.kind,
|
|
1773
|
+
importedSymbols: /* @__PURE__ */ new Set(),
|
|
1774
|
+
viaIndexOnly: true
|
|
1775
|
+
};
|
|
1776
|
+
for (const symbol of symbolsOfImportClause(clauseOf(imported.statement))) {
|
|
1777
|
+
accumulator.importedSymbols.add(symbol);
|
|
1778
|
+
}
|
|
1779
|
+
if (piercesTheIndex(imported.specifier)) {
|
|
1780
|
+
accumulator.viaIndexOnly = false;
|
|
1781
|
+
findings.push(
|
|
1782
|
+
ogygieError({
|
|
1783
|
+
code: "FEATURE_BOUNDARY_VIOLATION",
|
|
1784
|
+
message: `"${entry.name}" imports an internal file of "${target.name}" instead of its index.ts.`,
|
|
1785
|
+
file: filePath,
|
|
1786
|
+
cause: `import from "${imported.specifier}" bypasses the public index of ${target.name}`,
|
|
1787
|
+
suggestedFix: `Export the needed symbols from the index of ${target.name} and import them from there.`
|
|
1788
|
+
})
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
dependents.set(entry.name, accumulator);
|
|
1792
|
+
}
|
|
1793
|
+
if (isTest(file.path)) continue;
|
|
1794
|
+
if (entry.appType === "front" || entry.appType === "gateway") continue;
|
|
1795
|
+
for (const table of ownedTables) {
|
|
1796
|
+
if (!content.includes(`"${table}"`)) continue;
|
|
1797
|
+
tableReadersOutsideOwner.push({ table, feature: entry.name, file: filePath });
|
|
1798
|
+
findings.push(
|
|
1799
|
+
ogygieError({
|
|
1800
|
+
code: "TABLE_REFERENCED_OUTSIDE_OWNER",
|
|
1801
|
+
message: `Table "${table}" is owned by "${target.name}" but referenced by production code of "${entry.name}".`,
|
|
1802
|
+
file: filePath,
|
|
1803
|
+
cause: `"${table}" found in a non-test file outside its owner`,
|
|
1804
|
+
suggestedFix: `Go through the public contracts of ${target.name} instead of touching the table \u2014 one table, one owner.`
|
|
1805
|
+
})
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
findings.push(...manifestDependencyFindings(deps.manifest, target, dependents));
|
|
1811
|
+
const feature = deps.manifest.features.find((candidate) => candidate.name === target.name);
|
|
1812
|
+
const report = {
|
|
1813
|
+
target: { name: target.name, kind: target.kind, folder: target.folder ?? "" },
|
|
1814
|
+
dependents: [...dependents.entries()].map(([name, accumulator]) => ({
|
|
1815
|
+
name,
|
|
1816
|
+
kind: accumulator.kind,
|
|
1817
|
+
importedSymbols: [...accumulator.importedSymbols].sort(),
|
|
1818
|
+
viaIndexOnly: accumulator.viaIndexOnly
|
|
1819
|
+
})).sort((left, right) => left.name < right.name ? -1 : 1),
|
|
1820
|
+
routes: (feature?.routes ?? []).map((route) => ({
|
|
1821
|
+
method: route.method,
|
|
1822
|
+
path: route.path,
|
|
1823
|
+
description: route.description
|
|
1824
|
+
})),
|
|
1825
|
+
ownedTables,
|
|
1826
|
+
tableReadersOutsideOwner,
|
|
1827
|
+
findings,
|
|
1828
|
+
testCommand: testCommandFor(target, [...dependents.keys()], entries),
|
|
1829
|
+
scope: {
|
|
1830
|
+
scannedFolders: scannedFolders.sort(),
|
|
1831
|
+
coveredKinds: [...new Set(entries.map((entry) => entry.kind))],
|
|
1832
|
+
outOfReach: [...outOfReach, ...outOfReachOf(target, entries)]
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
return ImpactReportSchema.parse(report);
|
|
1836
|
+
}
|
|
1837
|
+
function clauseOf(statement) {
|
|
1838
|
+
const match = /\bimport\b([\s\S]*?)\bfrom\b/.exec(statement);
|
|
1839
|
+
return match?.[1] ?? "";
|
|
1840
|
+
}
|
|
1841
|
+
function tablesOf(manifest, featureName) {
|
|
1842
|
+
const feature = manifest.features.find((candidate) => candidate.name === featureName);
|
|
1843
|
+
return feature === void 0 ? [] : dbTableNamesOf(feature);
|
|
1844
|
+
}
|
|
1845
|
+
function outOfReachOf(target, entries) {
|
|
1846
|
+
const limits = [
|
|
1847
|
+
"code that no manifest entry declares \u2014 the map is the only chart",
|
|
1848
|
+
"anything outside this repo: no network, no npm registry"
|
|
1849
|
+
];
|
|
1850
|
+
const spared = entries.filter(
|
|
1851
|
+
(entry) => entry.appType === "front" || entry.appType === "gateway"
|
|
1852
|
+
);
|
|
1853
|
+
if (spared.length > 0 && target.kind === "feature") {
|
|
1854
|
+
limits.push(
|
|
1855
|
+
`table ownership was NOT checked in ${spared.length} front/gateway entr${spared.length === 1 ? "y" : "ies"} \u2014 that code holds no database client, so a table name there is a query key or a route`
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1858
|
+
if (target.kind === "framework-package" || target.kind === "package") {
|
|
1859
|
+
limits.unshift(
|
|
1860
|
+
`consumers of "${target.name}" OUTSIDE this repo \u2014 it is a package, and a published package is imported by projects this analysis cannot see`
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1863
|
+
return limits;
|
|
1864
|
+
}
|
|
1865
|
+
function manifestDependencyFindings(manifest, target, dependents) {
|
|
1866
|
+
const findings = [];
|
|
1867
|
+
const declared = manifest.features.filter(
|
|
1868
|
+
(feature) => feature.dependsOn.includes(target.name)
|
|
1869
|
+
);
|
|
1870
|
+
for (const feature of declared) {
|
|
1871
|
+
if (!dependents.has(feature.name)) {
|
|
1872
|
+
findings.push(
|
|
1873
|
+
ogygieError({
|
|
1874
|
+
code: "MANIFEST_DEPENDENCY_STALE",
|
|
1875
|
+
message: `Feature "${feature.name}" declares dependsOn "${target.name}" but never imports it.`,
|
|
1876
|
+
file: `ogygie.manifest.json#features.${feature.name}`,
|
|
1877
|
+
cause: `dependsOn contains "${target.name}", no import of it found in its code`,
|
|
1878
|
+
suggestedFix: "Remove the stale dependsOn entry, or make the dependency real through the entry's index.ts."
|
|
1879
|
+
})
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
for (const [name, accumulator] of dependents) {
|
|
1884
|
+
if (accumulator.kind !== "feature") continue;
|
|
1885
|
+
if (declared.some((feature) => feature.name === name)) continue;
|
|
1886
|
+
findings.push(
|
|
1887
|
+
ogygieError({
|
|
1888
|
+
code: "MANIFEST_DEPENDENCY_UNDECLARED",
|
|
1889
|
+
message: `Feature "${name}" imports "${target.name}" but does not declare it in dependsOn.`,
|
|
1890
|
+
file: `ogygie.manifest.json#features.${name}`,
|
|
1891
|
+
cause: `imports of "${target.name}" found in its code, absent from its dependsOn`,
|
|
1892
|
+
suggestedFix: `Add "${target.name}" to the dependsOn of "${name}" \u2014 the manifest must never lie by omission.`
|
|
1893
|
+
})
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
return findings;
|
|
1897
|
+
}
|
|
1898
|
+
function testCommandFor(target, dependentNames, entries) {
|
|
1899
|
+
const folders = [target, ...entries.filter((entry) => dependentNames.includes(entry.name))].map((entry) => entry.folder).filter((folder) => folder !== null);
|
|
1900
|
+
return ["pnpm test --", ...[...new Set(folders)].sort()].join(" ");
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// src/server/schemas.ts
|
|
1556
1904
|
var SpecStatusSchema = SpecFrontmatterSchema.shape.status;
|
|
1557
|
-
var ReadableSpecSummarySchema =
|
|
1905
|
+
var ReadableSpecSummarySchema = z7.object({
|
|
1558
1906
|
id: FeatureNameSchema,
|
|
1559
1907
|
status: SpecStatusSchema,
|
|
1560
1908
|
priority: SpecFrontmatterSchema.shape.priority,
|
|
1561
|
-
depends_on:
|
|
1562
|
-
filePath:
|
|
1563
|
-
presentSections:
|
|
1909
|
+
depends_on: z7.array(FeatureNameSchema),
|
|
1910
|
+
filePath: z7.string(),
|
|
1911
|
+
presentSections: z7.array(z7.string()),
|
|
1564
1912
|
/** The first sentence of `# @intent`, for a list that reads (v1.20: the target-spec suggestions of the brief). */
|
|
1565
|
-
intent:
|
|
1913
|
+
intent: z7.string().default("")
|
|
1566
1914
|
});
|
|
1567
|
-
var UnreadableSpecSummarySchema =
|
|
1568
|
-
id:
|
|
1569
|
-
filePath:
|
|
1915
|
+
var UnreadableSpecSummarySchema = z7.object({
|
|
1916
|
+
id: z7.string(),
|
|
1917
|
+
filePath: z7.string(),
|
|
1570
1918
|
error: OgygieErrorSchema
|
|
1571
1919
|
});
|
|
1572
|
-
var SpecSummarySchema =
|
|
1920
|
+
var SpecSummarySchema = z7.union([
|
|
1573
1921
|
ReadableSpecSummarySchema,
|
|
1574
1922
|
UnreadableSpecSummarySchema
|
|
1575
1923
|
]);
|
|
1576
|
-
var ListSpecsOutputSchema =
|
|
1577
|
-
specs:
|
|
1924
|
+
var ListSpecsOutputSchema = z7.object({
|
|
1925
|
+
specs: z7.array(SpecSummarySchema)
|
|
1578
1926
|
});
|
|
1579
|
-
var SpecParamsSchema =
|
|
1927
|
+
var SpecParamsSchema = z7.object({
|
|
1580
1928
|
specId: FeatureNameSchema
|
|
1581
1929
|
});
|
|
1582
|
-
var GetSpecOutputSchema =
|
|
1930
|
+
var GetSpecOutputSchema = z7.object({
|
|
1583
1931
|
frontmatter: SpecFrontmatterSchema,
|
|
1584
1932
|
/** Markdown body after the frontmatter block, byte-exact. */
|
|
1585
|
-
body:
|
|
1586
|
-
presentSections:
|
|
1933
|
+
body: z7.string(),
|
|
1934
|
+
presentSections: z7.array(z7.string())
|
|
1587
1935
|
});
|
|
1588
|
-
var UpdateSpecInputSchema =
|
|
1936
|
+
var UpdateSpecInputSchema = z7.object({
|
|
1589
1937
|
frontmatter: SpecFrontmatterSchema,
|
|
1590
1938
|
/** Omitted ⇒ the existing markdown body is preserved byte-exact. */
|
|
1591
|
-
body:
|
|
1939
|
+
body: z7.string().max(2e5).optional()
|
|
1592
1940
|
});
|
|
1593
|
-
var UpdateSpecOutputSchema =
|
|
1941
|
+
var UpdateSpecOutputSchema = z7.object({
|
|
1594
1942
|
spec: ReadableSpecSummarySchema
|
|
1595
1943
|
});
|
|
1596
|
-
var CreateSpecInputSchema =
|
|
1944
|
+
var CreateSpecInputSchema = z7.object({
|
|
1597
1945
|
id: FeatureNameSchema.max(80)
|
|
1598
1946
|
});
|
|
1599
|
-
var CreateSpecOutputSchema =
|
|
1947
|
+
var CreateSpecOutputSchema = z7.object({
|
|
1600
1948
|
spec: ReadableSpecSummarySchema
|
|
1601
1949
|
});
|
|
1602
|
-
var GetManifestOutputSchema =
|
|
1950
|
+
var GetManifestOutputSchema = z7.object({
|
|
1603
1951
|
manifest: OgygieManifestSchema
|
|
1604
1952
|
});
|
|
1605
|
-
var BriefAttachmentSchema =
|
|
1606
|
-
name:
|
|
1607
|
-
contentBase64:
|
|
1953
|
+
var BriefAttachmentSchema = z7.object({
|
|
1954
|
+
name: z7.string().min(1).max(200),
|
|
1955
|
+
contentBase64: z7.string().min(1).max(57e5)
|
|
1608
1956
|
});
|
|
1609
|
-
var BriefAnswerSchema =
|
|
1610
|
-
var BriefInputSchema =
|
|
1611
|
-
brief:
|
|
1957
|
+
var BriefAnswerSchema = z7.object({ questionId: FeatureNameSchema, answer: z7.string().min(1).max(2e3) });
|
|
1958
|
+
var BriefInputSchema = z7.object({
|
|
1959
|
+
brief: z7.string().min(50).max(2e4),
|
|
1612
1960
|
/** Absent: the model names the new spec. Present and existing: an amendment. */
|
|
1613
1961
|
specId: FeatureNameSchema.optional(),
|
|
1614
1962
|
/** The client's material — a mock-up, a workbook, a document; five at most, formats md, txt, csv, json, html, xlsx, pdf. */
|
|
1615
|
-
attachments:
|
|
1963
|
+
attachments: z7.array(BriefAttachmentSchema).max(5).default([]),
|
|
1616
1964
|
/** D57: the answers of the past rounds of questions, three rounds at most. */
|
|
1617
|
-
answers:
|
|
1965
|
+
answers: z7.array(BriefAnswerSchema).max(15).default([]),
|
|
1618
1966
|
/** D57: the round this send opens; at 3 the draft is asked without a question. */
|
|
1619
|
-
round:
|
|
1967
|
+
round: z7.number().int().min(1).max(4).default(1),
|
|
1620
1968
|
/** D57: « Passer les questions » — the draft is asked, the unknown becomes an assumption. */
|
|
1621
|
-
skipQuestions:
|
|
1969
|
+
skipQuestions: z7.boolean().default(false)
|
|
1622
1970
|
});
|
|
1623
|
-
var BriefQuestionSchema =
|
|
1971
|
+
var BriefQuestionSchema = z7.object({
|
|
1624
1972
|
/** kebab-case, stable from one read to the next. */
|
|
1625
1973
|
id: FeatureNameSchema,
|
|
1626
|
-
text:
|
|
1974
|
+
text: z7.string().min(1),
|
|
1627
1975
|
/** The proposed answers; a free answer is always possible. */
|
|
1628
|
-
options:
|
|
1976
|
+
options: z7.array(z7.string().min(1)).min(2).max(4)
|
|
1629
1977
|
});
|
|
1630
|
-
var BriefOutputSchema =
|
|
1978
|
+
var BriefOutputSchema = z7.object({
|
|
1631
1979
|
specId: FeatureNameSchema,
|
|
1632
1980
|
/** 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:
|
|
1634
|
-
problemsFixed:
|
|
1981
|
+
attempts: z7.number().int().min(1).max(4),
|
|
1982
|
+
problemsFixed: z7.array(z7.string()),
|
|
1635
1983
|
/** No feature of the manifest carries this id: its first schemas.ts awaits the operator (D52, lot C). */
|
|
1636
|
-
isNewFeature:
|
|
1637
|
-
isAmendment:
|
|
1984
|
+
isNewFeature: z7.boolean(),
|
|
1985
|
+
isAmendment: z7.boolean(),
|
|
1638
1986
|
/** D53: the compositions the model proposed for a screen, the retained one (the head's) first; empty when the rubric is not a screen. */
|
|
1639
|
-
compositions:
|
|
1987
|
+
compositions: z7.array(z7.string()).default([]),
|
|
1640
1988
|
/** 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. */
|
|
1641
|
-
generation:
|
|
1989
|
+
generation: z7.object({ derivable: z7.number().int().nonnegative(), hole: z7.number().int().nonnegative() }).nullable().default(null),
|
|
1642
1990
|
/** D57: the points the model decided that the brief did not say — the `# Hypothèses` lines of the draft. */
|
|
1643
|
-
assumptions:
|
|
1644
|
-
usage:
|
|
1645
|
-
|
|
1991
|
+
assumptions: z7.array(z7.string()).default([]),
|
|
1992
|
+
usage: z7.array(
|
|
1993
|
+
z7.object({ model: z7.string(), inputTokens: z7.number().int().nonnegative(), outputTokens: z7.number().int().nonnegative() })
|
|
1646
1994
|
)
|
|
1647
1995
|
});
|
|
1648
|
-
var BriefStartOutputSchema =
|
|
1649
|
-
var BriefStageSchema =
|
|
1650
|
-
var BriefJobSchema =
|
|
1651
|
-
jobId:
|
|
1996
|
+
var BriefStartOutputSchema = z7.object({ jobId: z7.string() });
|
|
1997
|
+
var BriefStageSchema = z7.enum(["questions", "drafting", "judging", "compositions", "writing", "done", "failed"]);
|
|
1998
|
+
var BriefJobSchema = z7.object({
|
|
1999
|
+
jobId: z7.string(),
|
|
1652
2000
|
stage: BriefStageSchema,
|
|
1653
2001
|
/** On questions: the round in progress. */
|
|
1654
|
-
round:
|
|
2002
|
+
round: z7.number().int().min(1).max(3).optional(),
|
|
1655
2003
|
/** On drafting / judging: the attempt. */
|
|
1656
|
-
attempt:
|
|
2004
|
+
attempt: z7.number().int().min(1).max(4).optional(),
|
|
1657
2005
|
/** On compositions: k of N. */
|
|
1658
|
-
composition:
|
|
1659
|
-
elapsedMs:
|
|
2006
|
+
composition: z7.object({ done: z7.number().int().nonnegative(), total: z7.number().int().nonnegative() }).optional(),
|
|
2007
|
+
elapsedMs: z7.number().int().nonnegative(),
|
|
1660
2008
|
/** The P95 of B3: 90 000 ms. */
|
|
1661
|
-
budgetMs:
|
|
1662
|
-
questions:
|
|
2009
|
+
budgetMs: z7.number().int().positive(),
|
|
2010
|
+
questions: z7.array(BriefQuestionSchema).min(3).max(5).optional(),
|
|
1663
2011
|
result: BriefOutputSchema.optional(),
|
|
1664
2012
|
error: OgygieErrorSchema.optional()
|
|
1665
2013
|
});
|
|
1666
|
-
var
|
|
1667
|
-
var PullRequestStatusSchema = z6.object({
|
|
2014
|
+
var SpecImpactSchema = z7.object({
|
|
1668
2015
|
specId: FeatureNameSchema,
|
|
1669
|
-
branch:
|
|
1670
|
-
|
|
1671
|
-
|
|
2016
|
+
/** The branch read: spec/<id> while it exists, else the default branch. */
|
|
2017
|
+
ref: z7.string(),
|
|
2018
|
+
/** False: the rubric is not an entry of the map yet — the impact reads after the speculation. */
|
|
2019
|
+
onMap: z7.boolean(),
|
|
2020
|
+
report: ImpactReportSchema.nullable()
|
|
2021
|
+
});
|
|
2022
|
+
var ChecksStateSchema = z7.enum(["green", "red", "pending", "none"]);
|
|
2023
|
+
var PullRequestStatusSchema = z7.object({
|
|
2024
|
+
specId: FeatureNameSchema,
|
|
2025
|
+
branch: z7.string(),
|
|
2026
|
+
pullRequest: z7.object({ number: z7.number().int().positive(), url: z7.string(), headSha: z7.string(), title: z7.string() }).nullable(),
|
|
2027
|
+
specStatus: z7.enum(["draft", "validated", "implemented", "unreadable", "absent"]),
|
|
1672
2028
|
checks: ChecksStateSchema,
|
|
1673
|
-
checkRuns:
|
|
1674
|
-
mergeable:
|
|
1675
|
-
requiredRole:
|
|
1676
|
-
canMerge:
|
|
2029
|
+
checkRuns: z7.array(z7.object({ name: z7.string(), conclusion: z7.string().nullable() })),
|
|
2030
|
+
mergeable: z7.boolean().nullable(),
|
|
2031
|
+
requiredRole: z7.enum(["approver", "operator"]),
|
|
2032
|
+
canMerge: z7.boolean(),
|
|
1677
2033
|
/** Each thing standing between the PR and its merge, in the user's language. */
|
|
1678
|
-
blockers:
|
|
1679
|
-
labels:
|
|
2034
|
+
blockers: z7.array(z7.string()),
|
|
2035
|
+
labels: z7.array(z7.string()).default([]),
|
|
1680
2036
|
/** D52: the first schemas.ts of a new feature failed the gates twice — the operator has the hand (label needs-operator). */
|
|
1681
|
-
needsOperator:
|
|
2037
|
+
needsOperator: z7.boolean().default(false)
|
|
1682
2038
|
});
|
|
1683
|
-
var SpecPullRequestsSchema =
|
|
1684
|
-
pullRequests:
|
|
2039
|
+
var SpecPullRequestsSchema = z7.object({
|
|
2040
|
+
pullRequests: z7.array(z7.object({ specId: FeatureNameSchema, number: z7.number().int().positive(), url: z7.string(), checks: ChecksStateSchema, needsOperator: z7.boolean().default(false) }))
|
|
1685
2041
|
});
|
|
1686
|
-
var MergeOutputSchema =
|
|
2042
|
+
var MergeOutputSchema = z7.object({
|
|
1687
2043
|
specId: FeatureNameSchema,
|
|
1688
|
-
pullRequestNumber:
|
|
1689
|
-
mergeSha:
|
|
1690
|
-
mergedBy:
|
|
1691
|
-
mergedAt:
|
|
2044
|
+
pullRequestNumber: z7.number().int().positive(),
|
|
2045
|
+
mergeSha: z7.string(),
|
|
2046
|
+
mergedBy: z7.string(),
|
|
2047
|
+
mergedAt: z7.string()
|
|
1692
2048
|
});
|
|
1693
|
-
var EnvironmentNameSchema =
|
|
1694
|
-
var PromotableEnvironmentSchema =
|
|
1695
|
-
var EnvironmentParamsSchema =
|
|
1696
|
-
var EnvironmentsStatusSchema =
|
|
1697
|
-
environments:
|
|
1698
|
-
|
|
2049
|
+
var EnvironmentNameSchema = z7.enum(["development", "staging", "production"]);
|
|
2050
|
+
var PromotableEnvironmentSchema = z7.enum(["staging", "production"]);
|
|
2051
|
+
var EnvironmentParamsSchema = z7.object({ environment: PromotableEnvironmentSchema });
|
|
2052
|
+
var EnvironmentsStatusSchema = z7.object({
|
|
2053
|
+
environments: z7.array(
|
|
2054
|
+
z7.object({
|
|
1699
2055
|
name: EnvironmentNameSchema,
|
|
1700
|
-
ref:
|
|
1701
|
-
sha:
|
|
1702
|
-
deployment:
|
|
2056
|
+
ref: z7.string(),
|
|
2057
|
+
sha: z7.string().nullable(),
|
|
2058
|
+
deployment: z7.object({ id: z7.number().int(), state: z7.string().nullable(), createdAt: z7.string() }).nullable()
|
|
1703
2059
|
})
|
|
1704
2060
|
),
|
|
1705
2061
|
/** release/<date>.<n>, most recent first. */
|
|
1706
|
-
releases:
|
|
2062
|
+
releases: z7.array(z7.string())
|
|
1707
2063
|
});
|
|
1708
|
-
var
|
|
1709
|
-
|
|
2064
|
+
var EnvironmentReadParamsSchema = z7.object({ environment: EnvironmentNameSchema });
|
|
2065
|
+
var CommitReachSchema = z7.enum(["deployed", "forward", "backward", "unreachable"]);
|
|
2066
|
+
var EnvironmentCommitSchema = z7.object({
|
|
2067
|
+
sha: z7.string(),
|
|
2068
|
+
/** The first line of the message without its `spec: merge` prefix and its `(#n)` suffix. */
|
|
2069
|
+
title: z7.string(),
|
|
2070
|
+
specId: FeatureNameSchema.nullable(),
|
|
2071
|
+
author: z7.string(),
|
|
2072
|
+
authoredAt: z7.string(),
|
|
2073
|
+
releaseTag: z7.string().nullable(),
|
|
2074
|
+
reach: CommitReachSchema,
|
|
2075
|
+
/** The reason, in the client's language. */
|
|
2076
|
+
why: z7.string()
|
|
2077
|
+
});
|
|
2078
|
+
var EnvironmentCommitsSchema = z7.object({
|
|
2079
|
+
environment: EnvironmentNameSchema,
|
|
2080
|
+
ref: z7.string(),
|
|
2081
|
+
deployedSha: z7.string().nullable(),
|
|
2082
|
+
commits: z7.array(EnvironmentCommitSchema).max(30)
|
|
2083
|
+
});
|
|
2084
|
+
var PromotionInputSchema = z7.object({
|
|
2085
|
+
sha: z7.string().regex(/^[0-9a-f]{40}$/),
|
|
1710
2086
|
/** Operator only: a step back, forced, with its reason. */
|
|
1711
|
-
rollback:
|
|
1712
|
-
reason:
|
|
2087
|
+
rollback: z7.boolean().optional(),
|
|
2088
|
+
reason: z7.string().min(10).max(2e3).optional()
|
|
1713
2089
|
});
|
|
1714
|
-
var PromotionRecordSchema =
|
|
2090
|
+
var PromotionRecordSchema = z7.object({
|
|
1715
2091
|
environment: PromotableEnvironmentSchema,
|
|
1716
|
-
fromSha:
|
|
1717
|
-
toSha:
|
|
1718
|
-
actor:
|
|
1719
|
-
at:
|
|
1720
|
-
deploymentId:
|
|
1721
|
-
releaseTag:
|
|
1722
|
-
includedPullRequests:
|
|
1723
|
-
destructiveMigration:
|
|
1724
|
-
rollback:
|
|
2092
|
+
fromSha: z7.string().nullable(),
|
|
2093
|
+
toSha: z7.string(),
|
|
2094
|
+
actor: z7.string(),
|
|
2095
|
+
at: z7.string(),
|
|
2096
|
+
deploymentId: z7.number().int(),
|
|
2097
|
+
releaseTag: z7.string().nullable(),
|
|
2098
|
+
includedPullRequests: z7.array(z7.object({ number: z7.number().int(), specId: z7.string().nullable(), message: z7.string(), authoredAt: z7.string() })),
|
|
2099
|
+
destructiveMigration: z7.boolean(),
|
|
2100
|
+
rollback: z7.boolean()
|
|
1725
2101
|
});
|
|
1726
|
-
var ConsumptionQuerySchema =
|
|
1727
|
-
var ConsumptionReportSchema =
|
|
1728
|
-
month:
|
|
1729
|
-
specs:
|
|
1730
|
-
|
|
1731
|
-
specId:
|
|
1732
|
-
pullRequestNumber:
|
|
1733
|
-
runs:
|
|
1734
|
-
inputTokens:
|
|
1735
|
-
outputTokens:
|
|
1736
|
-
estimatedUsd:
|
|
1737
|
-
uncostedModels:
|
|
2102
|
+
var ConsumptionQuerySchema = z7.object({ month: z7.string().regex(/^\d{4}-\d{2}$/).optional() });
|
|
2103
|
+
var ConsumptionReportSchema = z7.object({
|
|
2104
|
+
month: z7.string(),
|
|
2105
|
+
specs: z7.array(
|
|
2106
|
+
z7.object({
|
|
2107
|
+
specId: z7.string(),
|
|
2108
|
+
pullRequestNumber: z7.number().int(),
|
|
2109
|
+
runs: z7.number().int(),
|
|
2110
|
+
inputTokens: z7.number().int(),
|
|
2111
|
+
outputTokens: z7.number().int(),
|
|
2112
|
+
estimatedUsd: z7.number(),
|
|
2113
|
+
uncostedModels: z7.array(z7.string())
|
|
1738
2114
|
})
|
|
1739
2115
|
),
|
|
1740
|
-
totals:
|
|
1741
|
-
budgetUsd:
|
|
1742
|
-
budgetRatio:
|
|
1743
|
-
warning:
|
|
2116
|
+
totals: z7.object({ runs: z7.number().int(), inputTokens: z7.number().int(), outputTokens: z7.number().int(), estimatedUsd: z7.number() }),
|
|
2117
|
+
budgetUsd: z7.number().nullable(),
|
|
2118
|
+
budgetRatio: z7.number().nullable(),
|
|
2119
|
+
warning: z7.boolean()
|
|
1744
2120
|
});
|
|
1745
|
-
var PreviewEntrySchema =
|
|
1746
|
-
var PreviewsListSchema =
|
|
2121
|
+
var PreviewEntrySchema = z7.object({ state: z7.string(), path: z7.string(), sizeBytes: z7.number().int().nonnegative() });
|
|
2122
|
+
var PreviewsListSchema = z7.object({
|
|
1747
2123
|
specId: FeatureNameSchema,
|
|
1748
2124
|
/** In the order of the states: empty, loading, nominal, error, then the rest — the pages of the composition the head declares, or of the root. */
|
|
1749
|
-
previews:
|
|
2125
|
+
previews: z7.array(PreviewEntrySchema),
|
|
1750
2126
|
/** D53: the composition the spec at the head declares under `# Design`; null when the screen has one composition. */
|
|
1751
|
-
composition:
|
|
2127
|
+
composition: z7.string().nullable().default(null),
|
|
1752
2128
|
/** D53: every composition the branch rendered, `selected` for the head's. */
|
|
1753
|
-
compositions:
|
|
2129
|
+
compositions: z7.array(z7.object({ name: z7.string(), selected: z7.boolean(), previews: z7.array(PreviewEntrySchema) })).default([])
|
|
1754
2130
|
});
|
|
1755
|
-
var SelectCompositionOutputSchema =
|
|
2131
|
+
var SelectCompositionOutputSchema = z7.object({
|
|
1756
2132
|
specId: FeatureNameSchema,
|
|
1757
|
-
composition:
|
|
2133
|
+
composition: z7.string(),
|
|
1758
2134
|
/** The head already declared it: nothing was written. */
|
|
1759
|
-
alreadySelected:
|
|
2135
|
+
alreadySelected: z7.boolean()
|
|
1760
2136
|
});
|
|
1761
2137
|
|
|
1762
2138
|
// src/server/routes.ts
|
|
@@ -1815,7 +2191,7 @@ async function registerStudioRoutes(app, resolver) {
|
|
|
1815
2191
|
}
|
|
1816
2192
|
|
|
1817
2193
|
// src/server/hosted/routes.ts
|
|
1818
|
-
import { z as
|
|
2194
|
+
import { z as z9 } from "zod";
|
|
1819
2195
|
|
|
1820
2196
|
// src/server/hosted/auth.ts
|
|
1821
2197
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
@@ -1929,53 +2305,53 @@ function unauthenticated(cause) {
|
|
|
1929
2305
|
}
|
|
1930
2306
|
|
|
1931
2307
|
// src/server/hosted/config.ts
|
|
1932
|
-
import { z as
|
|
1933
|
-
var HostRoleSchema =
|
|
1934
|
-
var MergePolicySchema =
|
|
1935
|
-
var HostUserSchema =
|
|
1936
|
-
email:
|
|
1937
|
-
displayName:
|
|
2308
|
+
import { z as z8 } from "zod";
|
|
2309
|
+
var HostRoleSchema = z8.enum(["reader", "author", "approver", "operator"]);
|
|
2310
|
+
var MergePolicySchema = z8.enum(["client", "operator"]);
|
|
2311
|
+
var HostUserSchema = z8.object({
|
|
2312
|
+
email: z8.email(),
|
|
2313
|
+
displayName: z8.string().min(1).max(120),
|
|
1938
2314
|
role: HostRoleSchema
|
|
1939
2315
|
});
|
|
1940
|
-
var HostConfigSchema =
|
|
2316
|
+
var HostConfigSchema = z8.object({
|
|
1941
2317
|
repo: GithubCoordinatesSchema,
|
|
1942
|
-
githubApp:
|
|
1943
|
-
appId:
|
|
1944
|
-
installationId:
|
|
1945
|
-
privateKeyPath:
|
|
2318
|
+
githubApp: z8.object({
|
|
2319
|
+
appId: z8.string().min(1),
|
|
2320
|
+
installationId: z8.string().min(1),
|
|
2321
|
+
privateKeyPath: z8.string().min(1)
|
|
1946
2322
|
}),
|
|
1947
|
-
users:
|
|
2323
|
+
users: z8.array(HostUserSchema).min(1),
|
|
1948
2324
|
/** "operator" at onboarding: the agency keeps the merge until it hands it over (D48). */
|
|
1949
2325
|
mergePolicy: MergePolicySchema.default("operator"),
|
|
1950
|
-
writeRateLimit:
|
|
1951
|
-
session:
|
|
1952
|
-
secretPath:
|
|
1953
|
-
ttlHours:
|
|
2326
|
+
writeRateLimit: z8.number().int().positive().default(20),
|
|
2327
|
+
session: z8.object({
|
|
2328
|
+
secretPath: z8.string().min(1),
|
|
2329
|
+
ttlHours: z8.number().int().positive().max(168).default(24)
|
|
1954
2330
|
}),
|
|
1955
|
-
mail:
|
|
1956
|
-
fromAddress:
|
|
2331
|
+
mail: z8.object({
|
|
2332
|
+
fromAddress: z8.email(),
|
|
1957
2333
|
/** HTTP endpoint of the mail provider: POST JSON { from, to, subject, text }. */
|
|
1958
|
-
providerUrl:
|
|
1959
|
-
apiKeyPath:
|
|
2334
|
+
providerUrl: z8.url(),
|
|
2335
|
+
apiKeyPath: z8.string().min(1)
|
|
1960
2336
|
}),
|
|
1961
2337
|
/** Base of the magic links — what the client's browser can reach. */
|
|
1962
|
-
publicUrl:
|
|
2338
|
+
publicUrl: z8.url(),
|
|
1963
2339
|
/** Lot D (D49): the two promotable refs. Optional until that lot lands. */
|
|
1964
|
-
environments:
|
|
1965
|
-
staging:
|
|
1966
|
-
production:
|
|
2340
|
+
environments: z8.object({
|
|
2341
|
+
staging: z8.object({ ref: z8.literal("env/staging") }),
|
|
2342
|
+
production: z8.object({ ref: z8.literal("env/production") })
|
|
1967
2343
|
}).optional(),
|
|
1968
2344
|
/**
|
|
1969
2345
|
* The client's model workspace (D50). Present, the brief → spec route exists
|
|
1970
2346
|
* and the launcher requires OGYGIE_MODEL_API_KEY; absent, the instance never
|
|
1971
2347
|
* calls a model. `specModel` is the `spec` model of D24.
|
|
1972
2348
|
*/
|
|
1973
|
-
model:
|
|
1974
|
-
specModel:
|
|
1975
|
-
workspaceId:
|
|
1976
|
-
monthlyBudgetUsd:
|
|
2349
|
+
model: z8.object({
|
|
2350
|
+
specModel: z8.string().min(1).default("claude-fable-5-1"),
|
|
2351
|
+
workspaceId: z8.string().min(1).optional(),
|
|
2352
|
+
monthlyBudgetUsd: z8.number().positive().optional(),
|
|
1977
2353
|
/** USD per million tokens, by model id — overrides the built-in table of the estimate (B8). */
|
|
1978
|
-
costPerMillionUsd:
|
|
2354
|
+
costPerMillionUsd: z8.record(z8.string(), z8.object({ input: z8.number().nonnegative(), output: z8.number().nonnegative() })).optional()
|
|
1979
2355
|
}).optional()
|
|
1980
2356
|
});
|
|
1981
2357
|
function parseHostConfig(raw, configPath) {
|
|
@@ -2003,13 +2379,13 @@ function hostedUserOf(request) {
|
|
|
2003
2379
|
if (user === void 0) return unauthenticated("no session attached to this request");
|
|
2004
2380
|
return user;
|
|
2005
2381
|
}
|
|
2006
|
-
var MagicLinkInputSchema =
|
|
2007
|
-
var CallbackQuerySchema =
|
|
2008
|
-
var MeOutputSchema =
|
|
2009
|
-
email:
|
|
2010
|
-
displayName:
|
|
2382
|
+
var MagicLinkInputSchema = z9.object({ email: z9.email() });
|
|
2383
|
+
var CallbackQuerySchema = z9.object({ token: z9.string().min(1) });
|
|
2384
|
+
var MeOutputSchema = z9.object({
|
|
2385
|
+
email: z9.email(),
|
|
2386
|
+
displayName: z9.string(),
|
|
2011
2387
|
role: HostRoleSchema,
|
|
2012
|
-
host:
|
|
2388
|
+
host: z9.object({ environments: z9.boolean(), mergePolicy: z9.enum(["client", "operator"]) })
|
|
2013
2389
|
});
|
|
2014
2390
|
async function registerHostedRoutes(app, deps) {
|
|
2015
2391
|
const sessions = {
|
|
@@ -2037,7 +2413,7 @@ async function registerHostedRoutes(app, deps) {
|
|
|
2037
2413
|
route.route({
|
|
2038
2414
|
method: "POST",
|
|
2039
2415
|
url: "/api/auth/magic-link",
|
|
2040
|
-
schema: { body: MagicLinkInputSchema, response: { 202:
|
|
2416
|
+
schema: { body: MagicLinkInputSchema, response: { 202: z9.object({ accepted: z9.literal(true) }) } },
|
|
2041
2417
|
handler: async (request, reply) => {
|
|
2042
2418
|
const user = findHostUser(deps.config, request.body.email);
|
|
2043
2419
|
if (user === null) {
|
|
@@ -2089,7 +2465,7 @@ ${link}
|
|
|
2089
2465
|
}
|
|
2090
2466
|
|
|
2091
2467
|
// src/server/hosted/brief-routes.ts
|
|
2092
|
-
import { z as
|
|
2468
|
+
import { z as z11 } from "zod";
|
|
2093
2469
|
|
|
2094
2470
|
// src/server/hosted/attachments.ts
|
|
2095
2471
|
import { createHash } from "node:crypto";
|
|
@@ -2322,7 +2698,7 @@ ${attachments.map((piece) => `- ${piece.name} (${piece.format}, ${sizeOf(piece.b
|
|
|
2322
2698
|
var attachmentDocumentsOf = (attachments) => attachments.flatMap((piece) => piece.kind === "pdf" ? [{ name: piece.name, data: piece.data }] : []);
|
|
2323
2699
|
|
|
2324
2700
|
// src/server/hosted/brief.ts
|
|
2325
|
-
import { z as
|
|
2701
|
+
import { z as z10 } from "zod";
|
|
2326
2702
|
import { checkSpec, generationGrammarCard, readManifestGeneration } from "@ogygie/generate";
|
|
2327
2703
|
var TEMPLATE_PATH2 = "specs/_TEMPLATE.spec.md";
|
|
2328
2704
|
var PRODUCT_RULES_PATH = "AGENTS.product.md";
|
|
@@ -2342,10 +2718,10 @@ var NO_MORE_QUESTIONS = "no more questions: write the draft now, and put what yo
|
|
|
2342
2718
|
async function readIfPresent(storage, path) {
|
|
2343
2719
|
return await storage.exists(path) ? storage.readFile(path) : null;
|
|
2344
2720
|
}
|
|
2345
|
-
var ManifestRouteSchema =
|
|
2346
|
-
var ManifestForPromptSchema =
|
|
2347
|
-
product:
|
|
2348
|
-
features:
|
|
2721
|
+
var ManifestRouteSchema = z10.looseObject({ method: z10.string(), path: z10.string(), schemaInput: z10.string().optional(), schemaOutput: z10.string().optional(), invalidates: z10.array(z10.string()).optional() });
|
|
2722
|
+
var ManifestForPromptSchema = z10.looseObject({
|
|
2723
|
+
product: z10.looseObject({ name: z10.string(), description: z10.string().optional() }).optional(),
|
|
2724
|
+
features: z10.array(z10.looseObject({ name: z10.string(), intent: z10.string().optional(), status: z10.string().optional(), spec: z10.string().nullable().optional(), routes: z10.array(ManifestRouteSchema).optional() })).optional()
|
|
2349
2725
|
});
|
|
2350
2726
|
async function manifestSummary(storage) {
|
|
2351
2727
|
const raw = await readIfPresent(storage, MANIFEST_PATH2);
|
|
@@ -2412,8 +2788,8 @@ function extractQuestions(text) {
|
|
|
2412
2788
|
} catch {
|
|
2413
2789
|
return { problem: "QUESTIONS: the JSON after the marker does not parse" };
|
|
2414
2790
|
}
|
|
2415
|
-
const shape =
|
|
2416
|
-
questions:
|
|
2791
|
+
const shape = z10.object({
|
|
2792
|
+
questions: z10.array(z10.object({ id: FeatureNameSchema, text: z10.string().trim().min(1), options: z10.array(z10.string().trim().min(1)).min(2).max(4) })).min(3).max(5)
|
|
2417
2793
|
});
|
|
2418
2794
|
const checked = shape.safeParse(parsed);
|
|
2419
2795
|
if (!checked.success) return { problem: `QUESTIONS: ${checked.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(" ; ")}` };
|
|
@@ -2866,7 +3242,7 @@ async function registerBriefRoutes(app, deps) {
|
|
|
2866
3242
|
route.route({
|
|
2867
3243
|
method: "GET",
|
|
2868
3244
|
url: "/api/briefs/:jobId",
|
|
2869
|
-
schema: { params:
|
|
3245
|
+
schema: { params: z11.object({ jobId: z11.string().min(1) }), response: { 200: BriefJobSchema } },
|
|
2870
3246
|
handler: async (request, reply) => {
|
|
2871
3247
|
hostedUserOf(request);
|
|
2872
3248
|
return reply.code(200).send(jobs.read(request.params.jobId));
|
|
@@ -2874,6 +3250,57 @@ async function registerBriefRoutes(app, deps) {
|
|
|
2874
3250
|
});
|
|
2875
3251
|
}
|
|
2876
3252
|
|
|
3253
|
+
// src/server/hosted/impact.ts
|
|
3254
|
+
var MANIFEST_PATH3 = "ogygie.manifest.json";
|
|
3255
|
+
async function specImpact(reader, ref, specId) {
|
|
3256
|
+
if (!await reader.exists(MANIFEST_PATH3)) {
|
|
3257
|
+
throwOgygieError({
|
|
3258
|
+
code: "MANIFEST_INVALID",
|
|
3259
|
+
message: `No ${MANIFEST_PATH3} on ${ref}: the impact cannot be read.`,
|
|
3260
|
+
file: MANIFEST_PATH3,
|
|
3261
|
+
cause: `missing on ${ref}`,
|
|
3262
|
+
suggestedFix: "Restore the manifest on the branch; the impact reads the map first."
|
|
3263
|
+
});
|
|
3264
|
+
}
|
|
3265
|
+
const parsed = OgygieManifestSchema.safeParse(JSON.parse(await reader.readFile(MANIFEST_PATH3)));
|
|
3266
|
+
if (!parsed.success) {
|
|
3267
|
+
return throwOgygieError(
|
|
3268
|
+
{
|
|
3269
|
+
code: "MANIFEST_INVALID",
|
|
3270
|
+
message: `The manifest on ${ref} does not validate.`,
|
|
3271
|
+
file: MANIFEST_PATH3,
|
|
3272
|
+
cause: parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(" ; "),
|
|
3273
|
+
suggestedFix: "Fix the manifest on the branch (ogygie check names the field)."
|
|
3274
|
+
},
|
|
3275
|
+
422
|
|
3276
|
+
);
|
|
3277
|
+
}
|
|
3278
|
+
const onMap = parsed.data.features.some((feature) => feature.name === specId);
|
|
3279
|
+
if (!onMap) return { specId, ref, onMap: false, report: null };
|
|
3280
|
+
const report = await analyzeImpact({ manifest: parsed.data, adapter: reader }, specId);
|
|
3281
|
+
return { specId, ref, onMap: true, report };
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3284
|
+
// src/server/hosted/impact-routes.ts
|
|
3285
|
+
function knowsBranches(storage) {
|
|
3286
|
+
return typeof storage.readerForSpec === "function";
|
|
3287
|
+
}
|
|
3288
|
+
async function registerImpactRoutes(app, deps) {
|
|
3289
|
+
const route = app.withTypeProvider();
|
|
3290
|
+
route.route({
|
|
3291
|
+
method: "GET",
|
|
3292
|
+
url: "/api/specs/:specId/impact",
|
|
3293
|
+
schema: { params: SpecParamsSchema, response: { 200: SpecImpactSchema } },
|
|
3294
|
+
handler: async (request, reply) => {
|
|
3295
|
+
const user = hostedUserOf(request);
|
|
3296
|
+
const storage = deps.storageFor(user);
|
|
3297
|
+
const { specId } = request.params;
|
|
3298
|
+
const located = knowsBranches(storage) ? await storage.readerForSpec(specId) : { reader: storage, ref: deps.defaultBranch };
|
|
3299
|
+
return reply.code(200).send(await specImpact(located.reader, located.ref, specId));
|
|
3300
|
+
}
|
|
3301
|
+
});
|
|
3302
|
+
}
|
|
3303
|
+
|
|
2877
3304
|
// src/server/hosted/previews.ts
|
|
2878
3305
|
var PREVIEWS_DIR = "previews";
|
|
2879
3306
|
var STATE_NAME = /^[a-z][a-z0-9-]*$/;
|
|
@@ -2979,6 +3406,10 @@ function createHostedSpecStorage(deps) {
|
|
|
2979
3406
|
}
|
|
2980
3407
|
const storage = {
|
|
2981
3408
|
lastProposal: null,
|
|
3409
|
+
async readerForSpec(specId) {
|
|
3410
|
+
const branch = specBranchOf(specId);
|
|
3411
|
+
return (await branches()).has(branch) ? { reader: readerOfBranch(branch), ref: branch } : { reader: defaultReader, ref: deps.github.defaultBranch };
|
|
3412
|
+
},
|
|
2982
3413
|
async readFile(path) {
|
|
2983
3414
|
return (await readerOf(path)).readFile(path);
|
|
2984
3415
|
},
|
|
@@ -3237,6 +3668,48 @@ async function environmentsStatus(deps) {
|
|
|
3237
3668
|
releases: releases.sort().reverse()
|
|
3238
3669
|
};
|
|
3239
3670
|
}
|
|
3671
|
+
var LINE_LENGTH = 30;
|
|
3672
|
+
function commitTitleOf(message) {
|
|
3673
|
+
const firstLine = message.split("\n")[0] ?? "";
|
|
3674
|
+
return firstLine.replace(/^spec: merge /u, "").replace(/\s*\(#\d+\)\s*$/u, "").trim();
|
|
3675
|
+
}
|
|
3676
|
+
function reachOf(environment, index, deployedIndex, stagingIndex) {
|
|
3677
|
+
if (index === deployedIndex) return { reach: "deployed", why: "d\xE9ploy\xE9" };
|
|
3678
|
+
if (environment === "development") return { reach: "unreachable", why: "le d\xE9veloppement suit la branche par d\xE9faut, il ne se promeut pas" };
|
|
3679
|
+
const newer = deployedIndex === -1 || index < deployedIndex;
|
|
3680
|
+
if (environment === "staging") {
|
|
3681
|
+
return newer ? { reach: "forward", why: "avance rapide depuis le d\xE9veloppement" } : { reach: "backward", why: "retour arri\xE8re : op\xE9rateur, avec un motif" };
|
|
3682
|
+
}
|
|
3683
|
+
if (index === stagingIndex && newer) return { reach: "forward", why: "le commit de la pr\xE9production" };
|
|
3684
|
+
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" };
|
|
3685
|
+
return { reach: "backward", why: "retour arri\xE8re : op\xE9rateur, avec un motif" };
|
|
3686
|
+
}
|
|
3687
|
+
async function environmentCommits(deps, environment) {
|
|
3688
|
+
const ref = environment === "development" ? deps.defaultBranch : ENVIRONMENT_REFS[environment];
|
|
3689
|
+
const [deployedSha, stagingSha, commits, tags] = await Promise.all([
|
|
3690
|
+
deps.deploy.getRefSha(ref),
|
|
3691
|
+
environment === "production" ? deps.deploy.getRefSha(ENVIRONMENT_REFS.staging) : Promise.resolve(null),
|
|
3692
|
+
deps.deploy.listCommits(deps.defaultBranch, LINE_LENGTH),
|
|
3693
|
+
deps.deploy.listReleaseTags("release/")
|
|
3694
|
+
]);
|
|
3695
|
+
const tagOf = new Map(tags.map((tag) => [tag.sha, tag.name]));
|
|
3696
|
+
const deployedIndex = commits.findIndex((commit) => commit.sha === deployedSha);
|
|
3697
|
+
const stagingIndex = stagingSha === null ? -1 : commits.findIndex((commit) => commit.sha === stagingSha);
|
|
3698
|
+
return {
|
|
3699
|
+
environment,
|
|
3700
|
+
ref,
|
|
3701
|
+
deployedSha,
|
|
3702
|
+
commits: commits.map((commit, index) => ({
|
|
3703
|
+
sha: commit.sha,
|
|
3704
|
+
title: commitTitleOf(commit.message),
|
|
3705
|
+
specId: MERGED_SPEC.exec(commit.message.split("\n")[0] ?? "")?.[1] ?? null,
|
|
3706
|
+
author: commit.author,
|
|
3707
|
+
authoredAt: commit.authoredAt,
|
|
3708
|
+
releaseTag: tagOf.get(commit.sha) ?? null,
|
|
3709
|
+
...reachOf(environment, index, deployedIndex, stagingIndex)
|
|
3710
|
+
}))
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3240
3713
|
function pullRequestsOf(commits) {
|
|
3241
3714
|
const included = [];
|
|
3242
3715
|
for (const commit of commits) {
|
|
@@ -3367,6 +3840,15 @@ async function registerPromotionRoutes(app, deps) {
|
|
|
3367
3840
|
return reply.code(200).send(await environmentsStatus(promotionDeps));
|
|
3368
3841
|
}
|
|
3369
3842
|
});
|
|
3843
|
+
route.route({
|
|
3844
|
+
method: "GET",
|
|
3845
|
+
url: "/api/environments/:environment/commits",
|
|
3846
|
+
schema: { params: EnvironmentReadParamsSchema, response: { 200: EnvironmentCommitsSchema } },
|
|
3847
|
+
handler: async (request, reply) => {
|
|
3848
|
+
hostedUserOf(request);
|
|
3849
|
+
return reply.code(200).send(await environmentCommits(promotionDeps, request.params.environment));
|
|
3850
|
+
}
|
|
3851
|
+
});
|
|
3370
3852
|
route.route({
|
|
3371
3853
|
method: "POST",
|
|
3372
3854
|
url: "/api/environments/:environment/promotions",
|
|
@@ -3380,17 +3862,17 @@ async function registerPromotionRoutes(app, deps) {
|
|
|
3380
3862
|
}
|
|
3381
3863
|
|
|
3382
3864
|
// src/server/hosted/usage.ts
|
|
3383
|
-
import { z as
|
|
3384
|
-
var UsageEntrySchema =
|
|
3385
|
-
at:
|
|
3865
|
+
import { z as z12 } from "zod";
|
|
3866
|
+
var UsageEntrySchema = z12.object({
|
|
3867
|
+
at: z12.string(),
|
|
3386
3868
|
/** brief → spec from the instance; `speculate` entries arrive with the generator's lot. */
|
|
3387
|
-
action:
|
|
3388
|
-
attempt:
|
|
3389
|
-
model:
|
|
3390
|
-
inputTokens:
|
|
3391
|
-
outputTokens:
|
|
3392
|
-
actor:
|
|
3393
|
-
specId:
|
|
3869
|
+
action: z12.enum(["brief", "speculate"]),
|
|
3870
|
+
attempt: z12.number().int().positive().optional(),
|
|
3871
|
+
model: z12.string(),
|
|
3872
|
+
inputTokens: z12.number().int().nonnegative(),
|
|
3873
|
+
outputTokens: z12.number().int().nonnegative(),
|
|
3874
|
+
actor: z12.string().optional(),
|
|
3875
|
+
specId: z12.string().optional()
|
|
3394
3876
|
});
|
|
3395
3877
|
var USAGE_MARKER = "<!-- ogygie-usage -->";
|
|
3396
3878
|
var DEFAULT_MODEL_COSTS = {
|
|
@@ -3425,7 +3907,7 @@ function parseUsageComment(body) {
|
|
|
3425
3907
|
const block = /```json\n([\s\S]*?)\n```/.exec(body)?.[1];
|
|
3426
3908
|
if (block === void 0) return [];
|
|
3427
3909
|
try {
|
|
3428
|
-
const parsed =
|
|
3910
|
+
const parsed = z12.array(UsageEntrySchema).safeParse(JSON.parse(block));
|
|
3429
3911
|
return parsed.success ? parsed.data : [];
|
|
3430
3912
|
} catch {
|
|
3431
3913
|
return [];
|
|
@@ -3500,9 +3982,9 @@ async function registerConsumptionRoutes(app, deps) {
|
|
|
3500
3982
|
}
|
|
3501
3983
|
|
|
3502
3984
|
// src/server/hosted/preview-routes.ts
|
|
3503
|
-
import { z as
|
|
3504
|
-
var PreviewParamsSchema = SpecParamsSchema.extend({ state:
|
|
3505
|
-
var CompositionPreviewParamsSchema = PreviewParamsSchema.extend({ composition:
|
|
3985
|
+
import { z as z13 } from "zod";
|
|
3986
|
+
var PreviewParamsSchema = SpecParamsSchema.extend({ state: z13.string().min(1).max(80) });
|
|
3987
|
+
var CompositionPreviewParamsSchema = PreviewParamsSchema.extend({ composition: z13.string().min(1).max(80) });
|
|
3506
3988
|
var PREVIEW_CSP = "sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:";
|
|
3507
3989
|
async function registerPreviewRoutes(app, deps) {
|
|
3508
3990
|
const route = app.withTypeProvider();
|
|
@@ -3544,7 +4026,7 @@ async function registerPreviewRoutes(app, deps) {
|
|
|
3544
4026
|
}
|
|
3545
4027
|
|
|
3546
4028
|
// src/server/hosted/composition-routes.ts
|
|
3547
|
-
import { z as
|
|
4029
|
+
import { z as z14 } from "zod";
|
|
3548
4030
|
|
|
3549
4031
|
// src/server/hosted/compositions.ts
|
|
3550
4032
|
var COMPOSITION_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
@@ -3588,7 +4070,7 @@ La composition \xAB ${composition} \xBB est retenue parmi celles propos\xE9es ;
|
|
|
3588
4070
|
}
|
|
3589
4071
|
|
|
3590
4072
|
// src/server/hosted/composition-routes.ts
|
|
3591
|
-
var CompositionParamsSchema = SpecParamsSchema.extend({ composition:
|
|
4073
|
+
var CompositionParamsSchema = SpecParamsSchema.extend({ composition: z14.string().min(1).max(80) });
|
|
3592
4074
|
async function registerCompositionRoutes(app, deps) {
|
|
3593
4075
|
const route = app.withTypeProvider();
|
|
3594
4076
|
route.route({
|
|
@@ -3644,6 +4126,7 @@ async function createStudioApp(options) {
|
|
|
3644
4126
|
await registerHostedRoutes(app, hosted);
|
|
3645
4127
|
await registerStudioRoutes(app, (request) => ({ storage: hosted.storageFor(hostedUserOf(request)) }));
|
|
3646
4128
|
await registerPreviewRoutes(app, { storageFor: hosted.storageFor });
|
|
4129
|
+
await registerImpactRoutes(app, { storageFor: hosted.storageFor, defaultBranch: hosted.config.repo.defaultBranch });
|
|
3647
4130
|
if (hosted.writer !== void 0) {
|
|
3648
4131
|
await registerBriefRoutes(app, { writer: hosted.writer, storageFor: hosted.storageFor, log: hosted.log, usage: hosted.usage, now: hosted.now });
|
|
3649
4132
|
}
|
|
@@ -4082,6 +4565,29 @@ function createGithubDeployClient(deps) {
|
|
|
4082
4565
|
commits: (answer?.commits ?? []).map((entry) => ({ sha: entry.sha, message: entry.commit.message, authoredAt: entry.commit.author.date }))
|
|
4083
4566
|
};
|
|
4084
4567
|
},
|
|
4568
|
+
async listReleaseTags(prefix) {
|
|
4569
|
+
const refs = await request("GET", `${repoUrl}/git/matching-refs/tags/${prefix}`, void 0) ?? [];
|
|
4570
|
+
const tags = [];
|
|
4571
|
+
for (const entry of refs) {
|
|
4572
|
+
const name = entry.ref.replace(/^refs\/tags\//u, "");
|
|
4573
|
+
if (entry.object.type !== "tag") {
|
|
4574
|
+
tags.push({ name, sha: entry.object.sha });
|
|
4575
|
+
continue;
|
|
4576
|
+
}
|
|
4577
|
+
const tag = await request("GET", `${repoUrl}/git/tags/${entry.object.sha}`, void 0, true);
|
|
4578
|
+
if (tag !== null) tags.push({ name, sha: tag.object.sha });
|
|
4579
|
+
}
|
|
4580
|
+
return tags;
|
|
4581
|
+
},
|
|
4582
|
+
async listCommits(ref, perPage) {
|
|
4583
|
+
const answer = await request(
|
|
4584
|
+
"GET",
|
|
4585
|
+
`${repoUrl}/commits?sha=${encodeURIComponent(ref)}&per_page=${Math.min(Math.max(perPage, 1), 100)}`,
|
|
4586
|
+
void 0,
|
|
4587
|
+
true
|
|
4588
|
+
);
|
|
4589
|
+
return (answer ?? []).map((entry) => ({ sha: entry.sha, message: entry.commit.message, authoredAt: entry.commit.author.date, author: entry.commit.author.name }));
|
|
4590
|
+
},
|
|
4085
4591
|
async updateRef(ref, sha, force) {
|
|
4086
4592
|
await request("PATCH", `${repoUrl}/git/refs/heads/${encodeURIComponent(ref)}`, { sha, force });
|
|
4087
4593
|
},
|