@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 specIdOf = (specPath) => (specPath.split("/").pop() ?? specPath).replace(/\.spec\.md$/, "");
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 = specIdOf(packageEntry.spec);
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 = z6.object({
1905
+ var ReadableSpecSummarySchema = z7.object({
1558
1906
  id: FeatureNameSchema,
1559
1907
  status: SpecStatusSchema,
1560
1908
  priority: SpecFrontmatterSchema.shape.priority,
1561
- depends_on: z6.array(FeatureNameSchema),
1562
- filePath: z6.string(),
1563
- presentSections: z6.array(z6.string()),
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: z6.string().default("")
1913
+ intent: z7.string().default("")
1566
1914
  });
1567
- var UnreadableSpecSummarySchema = z6.object({
1568
- id: z6.string(),
1569
- filePath: z6.string(),
1915
+ var UnreadableSpecSummarySchema = z7.object({
1916
+ id: z7.string(),
1917
+ filePath: z7.string(),
1570
1918
  error: OgygieErrorSchema
1571
1919
  });
1572
- var SpecSummarySchema = z6.union([
1920
+ var SpecSummarySchema = z7.union([
1573
1921
  ReadableSpecSummarySchema,
1574
1922
  UnreadableSpecSummarySchema
1575
1923
  ]);
1576
- var ListSpecsOutputSchema = z6.object({
1577
- specs: z6.array(SpecSummarySchema)
1924
+ var ListSpecsOutputSchema = z7.object({
1925
+ specs: z7.array(SpecSummarySchema)
1578
1926
  });
1579
- var SpecParamsSchema = z6.object({
1927
+ var SpecParamsSchema = z7.object({
1580
1928
  specId: FeatureNameSchema
1581
1929
  });
1582
- var GetSpecOutputSchema = z6.object({
1930
+ var GetSpecOutputSchema = z7.object({
1583
1931
  frontmatter: SpecFrontmatterSchema,
1584
1932
  /** Markdown body after the frontmatter block, byte-exact. */
1585
- body: z6.string(),
1586
- presentSections: z6.array(z6.string())
1933
+ body: z7.string(),
1934
+ presentSections: z7.array(z7.string())
1587
1935
  });
1588
- var UpdateSpecInputSchema = z6.object({
1936
+ var UpdateSpecInputSchema = z7.object({
1589
1937
  frontmatter: SpecFrontmatterSchema,
1590
1938
  /** Omitted ⇒ the existing markdown body is preserved byte-exact. */
1591
- body: z6.string().max(2e5).optional()
1939
+ body: z7.string().max(2e5).optional()
1592
1940
  });
1593
- var UpdateSpecOutputSchema = z6.object({
1941
+ var UpdateSpecOutputSchema = z7.object({
1594
1942
  spec: ReadableSpecSummarySchema
1595
1943
  });
1596
- var CreateSpecInputSchema = z6.object({
1944
+ var CreateSpecInputSchema = z7.object({
1597
1945
  id: FeatureNameSchema.max(80)
1598
1946
  });
1599
- var CreateSpecOutputSchema = z6.object({
1947
+ var CreateSpecOutputSchema = z7.object({
1600
1948
  spec: ReadableSpecSummarySchema
1601
1949
  });
1602
- var GetManifestOutputSchema = z6.object({
1950
+ var GetManifestOutputSchema = z7.object({
1603
1951
  manifest: OgygieManifestSchema
1604
1952
  });
1605
- var BriefAttachmentSchema = z6.object({
1606
- name: z6.string().min(1).max(200),
1607
- contentBase64: z6.string().min(1).max(57e5)
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 = z6.object({ questionId: FeatureNameSchema, answer: z6.string().min(1).max(2e3) });
1610
- var BriefInputSchema = z6.object({
1611
- brief: z6.string().min(50).max(2e4),
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: z6.array(BriefAttachmentSchema).max(5).default([]),
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: z6.array(BriefAnswerSchema).max(15).default([]),
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: z6.number().int().min(1).max(4).default(1),
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: z6.boolean().default(false)
1969
+ skipQuestions: z7.boolean().default(false)
1622
1970
  });
1623
- var BriefQuestionSchema = z6.object({
1971
+ var BriefQuestionSchema = z7.object({
1624
1972
  /** kebab-case, stable from one read to the next. */
1625
1973
  id: FeatureNameSchema,
1626
- text: z6.string().min(1),
1974
+ text: z7.string().min(1),
1627
1975
  /** The proposed answers; a free answer is always possible. */
1628
- options: z6.array(z6.string().min(1)).min(2).max(4)
1976
+ options: z7.array(z7.string().min(1)).min(2).max(4)
1629
1977
  });
1630
- var BriefOutputSchema = z6.object({
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: z6.number().int().min(1).max(4),
1634
- problemsFixed: z6.array(z6.string()),
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: z6.boolean(),
1637
- isAmendment: z6.boolean(),
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: z6.array(z6.string()).default([]),
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: z6.object({ derivable: z6.number().int().nonnegative(), hole: z6.number().int().nonnegative() }).nullable().default(null),
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: z6.array(z6.string()).default([]),
1644
- usage: z6.array(
1645
- z6.object({ model: z6.string(), inputTokens: z6.number().int().nonnegative(), outputTokens: z6.number().int().nonnegative() })
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 = 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(),
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: z6.number().int().min(1).max(3).optional(),
2002
+ round: z7.number().int().min(1).max(3).optional(),
1655
2003
  /** On drafting / judging: the attempt. */
1656
- attempt: z6.number().int().min(1).max(4).optional(),
2004
+ attempt: z7.number().int().min(1).max(4).optional(),
1657
2005
  /** 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(),
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: z6.number().int().positive(),
1662
- questions: z6.array(BriefQuestionSchema).min(3).max(5).optional(),
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 ChecksStateSchema = z6.enum(["green", "red", "pending", "none"]);
1667
- var PullRequestStatusSchema = z6.object({
2014
+ var SpecImpactSchema = z7.object({
1668
2015
  specId: FeatureNameSchema,
1669
- branch: z6.string(),
1670
- pullRequest: z6.object({ number: z6.number().int().positive(), url: z6.string(), headSha: z6.string(), title: z6.string() }).nullable(),
1671
- specStatus: z6.enum(["draft", "validated", "implemented", "unreadable", "absent"]),
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: z6.array(z6.object({ name: z6.string(), conclusion: z6.string().nullable() })),
1674
- mergeable: z6.boolean().nullable(),
1675
- requiredRole: z6.enum(["approver", "operator"]),
1676
- canMerge: z6.boolean(),
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: z6.array(z6.string()),
1679
- labels: z6.array(z6.string()).default([]),
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: z6.boolean().default(false)
2037
+ needsOperator: z7.boolean().default(false)
1682
2038
  });
1683
- var SpecPullRequestsSchema = z6.object({
1684
- pullRequests: z6.array(z6.object({ specId: FeatureNameSchema, number: z6.number().int().positive(), url: z6.string(), checks: ChecksStateSchema, needsOperator: z6.boolean().default(false) }))
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 = z6.object({
2042
+ var MergeOutputSchema = z7.object({
1687
2043
  specId: FeatureNameSchema,
1688
- pullRequestNumber: z6.number().int().positive(),
1689
- mergeSha: z6.string(),
1690
- mergedBy: z6.string(),
1691
- mergedAt: z6.string()
2044
+ pullRequestNumber: z7.number().int().positive(),
2045
+ mergeSha: z7.string(),
2046
+ mergedBy: z7.string(),
2047
+ mergedAt: z7.string()
1692
2048
  });
1693
- var EnvironmentNameSchema = z6.enum(["development", "staging", "production"]);
1694
- var PromotableEnvironmentSchema = z6.enum(["staging", "production"]);
1695
- var EnvironmentParamsSchema = z6.object({ environment: PromotableEnvironmentSchema });
1696
- var EnvironmentsStatusSchema = z6.object({
1697
- environments: z6.array(
1698
- z6.object({
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: z6.string(),
1701
- sha: z6.string().nullable(),
1702
- deployment: z6.object({ id: z6.number().int(), state: z6.string().nullable(), createdAt: z6.string() }).nullable()
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: z6.array(z6.string())
2062
+ releases: z7.array(z7.string())
1707
2063
  });
1708
- var PromotionInputSchema = z6.object({
1709
- sha: z6.string().regex(/^[0-9a-f]{40}$/),
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: z6.boolean().optional(),
1712
- reason: z6.string().min(10).max(2e3).optional()
2087
+ rollback: z7.boolean().optional(),
2088
+ reason: z7.string().min(10).max(2e3).optional()
1713
2089
  });
1714
- var PromotionRecordSchema = z6.object({
2090
+ var PromotionRecordSchema = z7.object({
1715
2091
  environment: PromotableEnvironmentSchema,
1716
- fromSha: z6.string().nullable(),
1717
- toSha: z6.string(),
1718
- actor: z6.string(),
1719
- at: z6.string(),
1720
- deploymentId: z6.number().int(),
1721
- releaseTag: z6.string().nullable(),
1722
- includedPullRequests: z6.array(z6.object({ number: z6.number().int(), specId: z6.string().nullable(), message: z6.string(), authoredAt: z6.string() })),
1723
- destructiveMigration: z6.boolean(),
1724
- rollback: z6.boolean()
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 = z6.object({ month: z6.string().regex(/^\d{4}-\d{2}$/).optional() });
1727
- var ConsumptionReportSchema = z6.object({
1728
- month: z6.string(),
1729
- specs: z6.array(
1730
- z6.object({
1731
- specId: z6.string(),
1732
- pullRequestNumber: z6.number().int(),
1733
- runs: z6.number().int(),
1734
- inputTokens: z6.number().int(),
1735
- outputTokens: z6.number().int(),
1736
- estimatedUsd: z6.number(),
1737
- uncostedModels: z6.array(z6.string())
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: z6.object({ runs: z6.number().int(), inputTokens: z6.number().int(), outputTokens: z6.number().int(), estimatedUsd: z6.number() }),
1741
- budgetUsd: z6.number().nullable(),
1742
- budgetRatio: z6.number().nullable(),
1743
- warning: z6.boolean()
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 = z6.object({ state: z6.string(), path: z6.string(), sizeBytes: z6.number().int().nonnegative() });
1746
- var PreviewsListSchema = z6.object({
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: z6.array(PreviewEntrySchema),
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: z6.string().nullable().default(null),
2127
+ composition: z7.string().nullable().default(null),
1752
2128
  /** D53: every composition the branch rendered, `selected` for the head's. */
1753
- compositions: z6.array(z6.object({ name: z6.string(), selected: z6.boolean(), previews: z6.array(PreviewEntrySchema) })).default([])
2129
+ compositions: z7.array(z7.object({ name: z7.string(), selected: z7.boolean(), previews: z7.array(PreviewEntrySchema) })).default([])
1754
2130
  });
1755
- var SelectCompositionOutputSchema = z6.object({
2131
+ var SelectCompositionOutputSchema = z7.object({
1756
2132
  specId: FeatureNameSchema,
1757
- composition: z6.string(),
2133
+ composition: z7.string(),
1758
2134
  /** The head already declared it: nothing was written. */
1759
- alreadySelected: z6.boolean()
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 z8 } from "zod";
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 z7 } from "zod";
1933
- var HostRoleSchema = z7.enum(["reader", "author", "approver", "operator"]);
1934
- var MergePolicySchema = z7.enum(["client", "operator"]);
1935
- var HostUserSchema = z7.object({
1936
- email: z7.email(),
1937
- displayName: z7.string().min(1).max(120),
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 = z7.object({
2316
+ var HostConfigSchema = z8.object({
1941
2317
  repo: GithubCoordinatesSchema,
1942
- githubApp: z7.object({
1943
- appId: z7.string().min(1),
1944
- installationId: z7.string().min(1),
1945
- privateKeyPath: z7.string().min(1)
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: z7.array(HostUserSchema).min(1),
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: z7.number().int().positive().default(20),
1951
- session: z7.object({
1952
- secretPath: z7.string().min(1),
1953
- ttlHours: z7.number().int().positive().max(168).default(24)
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: z7.object({
1956
- fromAddress: z7.email(),
2331
+ mail: z8.object({
2332
+ fromAddress: z8.email(),
1957
2333
  /** HTTP endpoint of the mail provider: POST JSON { from, to, subject, text }. */
1958
- providerUrl: z7.url(),
1959
- apiKeyPath: z7.string().min(1)
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: z7.url(),
2338
+ publicUrl: z8.url(),
1963
2339
  /** Lot D (D49): the two promotable refs. Optional until that lot lands. */
1964
- environments: z7.object({
1965
- staging: z7.object({ ref: z7.literal("env/staging") }),
1966
- production: z7.object({ ref: z7.literal("env/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: z7.object({
1974
- specModel: z7.string().min(1).default("claude-fable-5-1"),
1975
- workspaceId: z7.string().min(1).optional(),
1976
- monthlyBudgetUsd: z7.number().positive().optional(),
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: z7.record(z7.string(), z7.object({ input: z7.number().nonnegative(), output: z7.number().nonnegative() })).optional()
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 = z8.object({ email: z8.email() });
2007
- var CallbackQuerySchema = z8.object({ token: z8.string().min(1) });
2008
- var MeOutputSchema = z8.object({
2009
- email: z8.email(),
2010
- displayName: z8.string(),
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: z8.object({ environments: z8.boolean(), mergePolicy: z8.enum(["client", "operator"]) })
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: z8.object({ accepted: z8.literal(true) }) } },
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 z10 } from "zod";
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 z9 } from "zod";
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 = z9.looseObject({ method: z9.string(), path: z9.string(), schemaInput: z9.string().optional(), schemaOutput: z9.string().optional(), invalidates: z9.array(z9.string()).optional() });
2346
- var ManifestForPromptSchema = z9.looseObject({
2347
- product: z9.looseObject({ name: z9.string(), description: z9.string().optional() }).optional(),
2348
- features: z9.array(z9.looseObject({ name: z9.string(), intent: z9.string().optional(), status: z9.string().optional(), spec: z9.string().nullable().optional(), routes: z9.array(ManifestRouteSchema).optional() })).optional()
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 = z9.object({
2416
- 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)
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: z10.object({ jobId: z10.string().min(1) }), response: { 200: BriefJobSchema } },
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 z11 } from "zod";
3384
- var UsageEntrySchema = z11.object({
3385
- at: z11.string(),
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: z11.enum(["brief", "speculate"]),
3388
- attempt: z11.number().int().positive().optional(),
3389
- model: z11.string(),
3390
- inputTokens: z11.number().int().nonnegative(),
3391
- outputTokens: z11.number().int().nonnegative(),
3392
- actor: z11.string().optional(),
3393
- specId: z11.string().optional()
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 = z11.array(UsageEntrySchema).safeParse(JSON.parse(block));
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 z12 } from "zod";
3504
- var PreviewParamsSchema = SpecParamsSchema.extend({ state: z12.string().min(1).max(80) });
3505
- var CompositionPreviewParamsSchema = PreviewParamsSchema.extend({ composition: z12.string().min(1).max(80) });
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 z13 } from "zod";
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: z13.string().min(1).max(80) });
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
  },