@ogygie/studio 0.28.0 → 0.29.1

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,231 +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({
2015
+ specId: FeatureNameSchema,
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({
1668
2024
  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"]),
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 EnvironmentReadParamsSchema = z6.object({ environment: EnvironmentNameSchema });
1709
- var CommitReachSchema = z6.enum(["deployed", "forward", "backward", "unreachable"]);
1710
- var EnvironmentCommitSchema = z6.object({
1711
- sha: z6.string(),
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(),
1712
2068
  /** The first line of the message without its `spec: merge` prefix and its `(#n)` suffix. */
1713
- title: z6.string(),
2069
+ title: z7.string(),
1714
2070
  specId: FeatureNameSchema.nullable(),
1715
- author: z6.string(),
1716
- authoredAt: z6.string(),
1717
- releaseTag: z6.string().nullable(),
2071
+ author: z7.string(),
2072
+ authoredAt: z7.string(),
2073
+ releaseTag: z7.string().nullable(),
1718
2074
  reach: CommitReachSchema,
1719
2075
  /** The reason, in the client's language. */
1720
- why: z6.string()
2076
+ why: z7.string()
1721
2077
  });
1722
- var EnvironmentCommitsSchema = z6.object({
2078
+ var EnvironmentCommitsSchema = z7.object({
1723
2079
  environment: EnvironmentNameSchema,
1724
- ref: z6.string(),
1725
- deployedSha: z6.string().nullable(),
1726
- commits: z6.array(EnvironmentCommitSchema).max(30)
2080
+ ref: z7.string(),
2081
+ deployedSha: z7.string().nullable(),
2082
+ commits: z7.array(EnvironmentCommitSchema).max(30)
1727
2083
  });
1728
- var PromotionInputSchema = z6.object({
1729
- sha: z6.string().regex(/^[0-9a-f]{40}$/),
2084
+ var PromotionInputSchema = z7.object({
2085
+ sha: z7.string().regex(/^[0-9a-f]{40}$/),
1730
2086
  /** Operator only: a step back, forced, with its reason. */
1731
- rollback: z6.boolean().optional(),
1732
- reason: z6.string().min(10).max(2e3).optional()
2087
+ rollback: z7.boolean().optional(),
2088
+ reason: z7.string().min(10).max(2e3).optional()
1733
2089
  });
1734
- var PromotionRecordSchema = z6.object({
2090
+ var PromotionRecordSchema = z7.object({
1735
2091
  environment: PromotableEnvironmentSchema,
1736
- fromSha: z6.string().nullable(),
1737
- toSha: z6.string(),
1738
- actor: z6.string(),
1739
- at: z6.string(),
1740
- deploymentId: z6.number().int(),
1741
- releaseTag: z6.string().nullable(),
1742
- includedPullRequests: z6.array(z6.object({ number: z6.number().int(), specId: z6.string().nullable(), message: z6.string(), authoredAt: z6.string() })),
1743
- destructiveMigration: z6.boolean(),
1744
- 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()
1745
2101
  });
1746
- var ConsumptionQuerySchema = z6.object({ month: z6.string().regex(/^\d{4}-\d{2}$/).optional() });
1747
- var ConsumptionReportSchema = z6.object({
1748
- month: z6.string(),
1749
- specs: z6.array(
1750
- z6.object({
1751
- specId: z6.string(),
1752
- pullRequestNumber: z6.number().int(),
1753
- runs: z6.number().int(),
1754
- inputTokens: z6.number().int(),
1755
- outputTokens: z6.number().int(),
1756
- estimatedUsd: z6.number(),
1757
- 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())
1758
2114
  })
1759
2115
  ),
1760
- totals: z6.object({ runs: z6.number().int(), inputTokens: z6.number().int(), outputTokens: z6.number().int(), estimatedUsd: z6.number() }),
1761
- budgetUsd: z6.number().nullable(),
1762
- budgetRatio: z6.number().nullable(),
1763
- 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()
1764
2120
  });
1765
- var PreviewEntrySchema = z6.object({ state: z6.string(), path: z6.string(), sizeBytes: z6.number().int().nonnegative() });
1766
- 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({
1767
2123
  specId: FeatureNameSchema,
1768
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. */
1769
- previews: z6.array(PreviewEntrySchema),
2125
+ previews: z7.array(PreviewEntrySchema),
1770
2126
  /** D53: the composition the spec at the head declares under `# Design`; null when the screen has one composition. */
1771
- composition: z6.string().nullable().default(null),
2127
+ composition: z7.string().nullable().default(null),
1772
2128
  /** D53: every composition the branch rendered, `selected` for the head's. */
1773
- 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([])
1774
2130
  });
1775
- var SelectCompositionOutputSchema = z6.object({
2131
+ var SelectCompositionOutputSchema = z7.object({
1776
2132
  specId: FeatureNameSchema,
1777
- composition: z6.string(),
2133
+ composition: z7.string(),
1778
2134
  /** The head already declared it: nothing was written. */
1779
- alreadySelected: z6.boolean()
2135
+ alreadySelected: z7.boolean()
1780
2136
  });
1781
2137
 
1782
2138
  // src/server/routes.ts
@@ -1835,7 +2191,7 @@ async function registerStudioRoutes(app, resolver) {
1835
2191
  }
1836
2192
 
1837
2193
  // src/server/hosted/routes.ts
1838
- import { z as z8 } from "zod";
2194
+ import { z as z9 } from "zod";
1839
2195
 
1840
2196
  // src/server/hosted/auth.ts
1841
2197
  import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
@@ -1949,53 +2305,53 @@ function unauthenticated(cause) {
1949
2305
  }
1950
2306
 
1951
2307
  // src/server/hosted/config.ts
1952
- import { z as z7 } from "zod";
1953
- var HostRoleSchema = z7.enum(["reader", "author", "approver", "operator"]);
1954
- var MergePolicySchema = z7.enum(["client", "operator"]);
1955
- var HostUserSchema = z7.object({
1956
- email: z7.email(),
1957
- 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),
1958
2314
  role: HostRoleSchema
1959
2315
  });
1960
- var HostConfigSchema = z7.object({
2316
+ var HostConfigSchema = z8.object({
1961
2317
  repo: GithubCoordinatesSchema,
1962
- githubApp: z7.object({
1963
- appId: z7.string().min(1),
1964
- installationId: z7.string().min(1),
1965
- 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)
1966
2322
  }),
1967
- users: z7.array(HostUserSchema).min(1),
2323
+ users: z8.array(HostUserSchema).min(1),
1968
2324
  /** "operator" at onboarding: the agency keeps the merge until it hands it over (D48). */
1969
2325
  mergePolicy: MergePolicySchema.default("operator"),
1970
- writeRateLimit: z7.number().int().positive().default(20),
1971
- session: z7.object({
1972
- secretPath: z7.string().min(1),
1973
- 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)
1974
2330
  }),
1975
- mail: z7.object({
1976
- fromAddress: z7.email(),
2331
+ mail: z8.object({
2332
+ fromAddress: z8.email(),
1977
2333
  /** HTTP endpoint of the mail provider: POST JSON { from, to, subject, text }. */
1978
- providerUrl: z7.url(),
1979
- apiKeyPath: z7.string().min(1)
2334
+ providerUrl: z8.url(),
2335
+ apiKeyPath: z8.string().min(1)
1980
2336
  }),
1981
2337
  /** Base of the magic links — what the client's browser can reach. */
1982
- publicUrl: z7.url(),
2338
+ publicUrl: z8.url(),
1983
2339
  /** Lot D (D49): the two promotable refs. Optional until that lot lands. */
1984
- environments: z7.object({
1985
- staging: z7.object({ ref: z7.literal("env/staging") }),
1986
- 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") })
1987
2343
  }).optional(),
1988
2344
  /**
1989
2345
  * The client's model workspace (D50). Present, the brief → spec route exists
1990
2346
  * and the launcher requires OGYGIE_MODEL_API_KEY; absent, the instance never
1991
2347
  * calls a model. `specModel` is the `spec` model of D24.
1992
2348
  */
1993
- model: z7.object({
1994
- specModel: z7.string().min(1).default("claude-fable-5-1"),
1995
- workspaceId: z7.string().min(1).optional(),
1996
- 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(),
1997
2353
  /** USD per million tokens, by model id — overrides the built-in table of the estimate (B8). */
1998
- 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()
1999
2355
  }).optional()
2000
2356
  });
2001
2357
  function parseHostConfig(raw, configPath) {
@@ -2023,13 +2379,13 @@ function hostedUserOf(request) {
2023
2379
  if (user === void 0) return unauthenticated("no session attached to this request");
2024
2380
  return user;
2025
2381
  }
2026
- var MagicLinkInputSchema = z8.object({ email: z8.email() });
2027
- var CallbackQuerySchema = z8.object({ token: z8.string().min(1) });
2028
- var MeOutputSchema = z8.object({
2029
- email: z8.email(),
2030
- 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(),
2031
2387
  role: HostRoleSchema,
2032
- host: z8.object({ environments: z8.boolean(), mergePolicy: z8.enum(["client", "operator"]) })
2388
+ host: z9.object({ environments: z9.boolean(), mergePolicy: z9.enum(["client", "operator"]) })
2033
2389
  });
2034
2390
  async function registerHostedRoutes(app, deps) {
2035
2391
  const sessions = {
@@ -2057,7 +2413,7 @@ async function registerHostedRoutes(app, deps) {
2057
2413
  route.route({
2058
2414
  method: "POST",
2059
2415
  url: "/api/auth/magic-link",
2060
- schema: { body: MagicLinkInputSchema, response: { 202: z8.object({ accepted: z8.literal(true) }) } },
2416
+ schema: { body: MagicLinkInputSchema, response: { 202: z9.object({ accepted: z9.literal(true) }) } },
2061
2417
  handler: async (request, reply) => {
2062
2418
  const user = findHostUser(deps.config, request.body.email);
2063
2419
  if (user === null) {
@@ -2109,7 +2465,7 @@ ${link}
2109
2465
  }
2110
2466
 
2111
2467
  // src/server/hosted/brief-routes.ts
2112
- import { z as z10 } from "zod";
2468
+ import { z as z11 } from "zod";
2113
2469
 
2114
2470
  // src/server/hosted/attachments.ts
2115
2471
  import { createHash } from "node:crypto";
@@ -2342,7 +2698,7 @@ ${attachments.map((piece) => `- ${piece.name} (${piece.format}, ${sizeOf(piece.b
2342
2698
  var attachmentDocumentsOf = (attachments) => attachments.flatMap((piece) => piece.kind === "pdf" ? [{ name: piece.name, data: piece.data }] : []);
2343
2699
 
2344
2700
  // src/server/hosted/brief.ts
2345
- import { z as z9 } from "zod";
2701
+ import { z as z10 } from "zod";
2346
2702
  import { checkSpec, generationGrammarCard, readManifestGeneration } from "@ogygie/generate";
2347
2703
  var TEMPLATE_PATH2 = "specs/_TEMPLATE.spec.md";
2348
2704
  var PRODUCT_RULES_PATH = "AGENTS.product.md";
@@ -2362,10 +2718,10 @@ var NO_MORE_QUESTIONS = "no more questions: write the draft now, and put what yo
2362
2718
  async function readIfPresent(storage, path) {
2363
2719
  return await storage.exists(path) ? storage.readFile(path) : null;
2364
2720
  }
2365
- var ManifestRouteSchema = z9.looseObject({ method: z9.string(), path: z9.string(), schemaInput: z9.string().optional(), schemaOutput: z9.string().optional(), invalidates: z9.array(z9.string()).optional() });
2366
- var ManifestForPromptSchema = z9.looseObject({
2367
- product: z9.looseObject({ name: z9.string(), description: z9.string().optional() }).optional(),
2368
- 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()
2369
2725
  });
2370
2726
  async function manifestSummary(storage) {
2371
2727
  const raw = await readIfPresent(storage, MANIFEST_PATH2);
@@ -2432,8 +2788,8 @@ function extractQuestions(text) {
2432
2788
  } catch {
2433
2789
  return { problem: "QUESTIONS: the JSON after the marker does not parse" };
2434
2790
  }
2435
- const shape = z9.object({
2436
- questions: z9.array(z9.object({ id: FeatureNameSchema, text: z9.string().trim().min(1), options: z9.array(z9.string().trim().min(1)).min(2).max(4) })).min(3).max(5)
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)
2437
2793
  });
2438
2794
  const checked = shape.safeParse(parsed);
2439
2795
  if (!checked.success) return { problem: `QUESTIONS: ${checked.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join(" ; ")}` };
@@ -2886,7 +3242,7 @@ async function registerBriefRoutes(app, deps) {
2886
3242
  route.route({
2887
3243
  method: "GET",
2888
3244
  url: "/api/briefs/:jobId",
2889
- 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 } },
2890
3246
  handler: async (request, reply) => {
2891
3247
  hostedUserOf(request);
2892
3248
  return reply.code(200).send(jobs.read(request.params.jobId));
@@ -2894,6 +3250,57 @@ async function registerBriefRoutes(app, deps) {
2894
3250
  });
2895
3251
  }
2896
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
+
2897
3304
  // src/server/hosted/previews.ts
2898
3305
  var PREVIEWS_DIR = "previews";
2899
3306
  var STATE_NAME = /^[a-z][a-z0-9-]*$/;
@@ -2999,6 +3406,10 @@ function createHostedSpecStorage(deps) {
2999
3406
  }
3000
3407
  const storage = {
3001
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
+ },
3002
3413
  async readFile(path) {
3003
3414
  return (await readerOf(path)).readFile(path);
3004
3415
  },
@@ -3451,17 +3862,17 @@ async function registerPromotionRoutes(app, deps) {
3451
3862
  }
3452
3863
 
3453
3864
  // src/server/hosted/usage.ts
3454
- import { z as z11 } from "zod";
3455
- var UsageEntrySchema = z11.object({
3456
- at: z11.string(),
3865
+ import { z as z12 } from "zod";
3866
+ var UsageEntrySchema = z12.object({
3867
+ at: z12.string(),
3457
3868
  /** brief → spec from the instance; `speculate` entries arrive with the generator's lot. */
3458
- action: z11.enum(["brief", "speculate"]),
3459
- attempt: z11.number().int().positive().optional(),
3460
- model: z11.string(),
3461
- inputTokens: z11.number().int().nonnegative(),
3462
- outputTokens: z11.number().int().nonnegative(),
3463
- actor: z11.string().optional(),
3464
- specId: z11.string().optional()
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()
3465
3876
  });
3466
3877
  var USAGE_MARKER = "<!-- ogygie-usage -->";
3467
3878
  var DEFAULT_MODEL_COSTS = {
@@ -3496,7 +3907,7 @@ function parseUsageComment(body) {
3496
3907
  const block = /```json\n([\s\S]*?)\n```/.exec(body)?.[1];
3497
3908
  if (block === void 0) return [];
3498
3909
  try {
3499
- const parsed = z11.array(UsageEntrySchema).safeParse(JSON.parse(block));
3910
+ const parsed = z12.array(UsageEntrySchema).safeParse(JSON.parse(block));
3500
3911
  return parsed.success ? parsed.data : [];
3501
3912
  } catch {
3502
3913
  return [];
@@ -3571,9 +3982,9 @@ async function registerConsumptionRoutes(app, deps) {
3571
3982
  }
3572
3983
 
3573
3984
  // src/server/hosted/preview-routes.ts
3574
- import { z as z12 } from "zod";
3575
- var PreviewParamsSchema = SpecParamsSchema.extend({ state: z12.string().min(1).max(80) });
3576
- var CompositionPreviewParamsSchema = PreviewParamsSchema.extend({ composition: z12.string().min(1).max(80) });
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) });
3577
3988
  var PREVIEW_CSP = "sandbox; default-src 'none'; style-src 'unsafe-inline'; img-src data:; font-src data:";
3578
3989
  async function registerPreviewRoutes(app, deps) {
3579
3990
  const route = app.withTypeProvider();
@@ -3615,7 +4026,7 @@ async function registerPreviewRoutes(app, deps) {
3615
4026
  }
3616
4027
 
3617
4028
  // src/server/hosted/composition-routes.ts
3618
- import { z as z13 } from "zod";
4029
+ import { z as z14 } from "zod";
3619
4030
 
3620
4031
  // src/server/hosted/compositions.ts
3621
4032
  var COMPOSITION_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
@@ -3659,7 +4070,7 @@ La composition \xAB ${composition} \xBB est retenue parmi celles propos\xE9es ;
3659
4070
  }
3660
4071
 
3661
4072
  // src/server/hosted/composition-routes.ts
3662
- var CompositionParamsSchema = SpecParamsSchema.extend({ composition: z13.string().min(1).max(80) });
4073
+ var CompositionParamsSchema = SpecParamsSchema.extend({ composition: z14.string().min(1).max(80) });
3663
4074
  async function registerCompositionRoutes(app, deps) {
3664
4075
  const route = app.withTypeProvider();
3665
4076
  route.route({
@@ -3715,6 +4126,7 @@ async function createStudioApp(options) {
3715
4126
  await registerHostedRoutes(app, hosted);
3716
4127
  await registerStudioRoutes(app, (request) => ({ storage: hosted.storageFor(hostedUserOf(request)) }));
3717
4128
  await registerPreviewRoutes(app, { storageFor: hosted.storageFor });
4129
+ await registerImpactRoutes(app, { storageFor: hosted.storageFor, defaultBranch: hosted.config.repo.defaultBranch });
3718
4130
  if (hosted.writer !== void 0) {
3719
4131
  await registerBriefRoutes(app, { writer: hosted.writer, storageFor: hosted.storageFor, log: hosted.log, usage: hosted.usage, now: hosted.now });
3720
4132
  }