@ascribed/astro 0.2.1-next.9 → 0.3.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.
Files changed (81) hide show
  1. package/README.md +41 -9
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +62 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/content.d.ts +30 -0
  6. package/dist/content.js +101 -2
  7. package/dist/content.js.map +1 -1
  8. package/dist/dev.d.ts +2 -1
  9. package/dist/dev.js +13 -3
  10. package/dist/dev.js.map +1 -1
  11. package/dist/files.d.ts +21 -4
  12. package/dist/files.js +46 -8
  13. package/dist/files.js.map +1 -1
  14. package/dist/head-check.d.ts +6 -0
  15. package/dist/head-check.js +28 -0
  16. package/dist/head-check.js.map +1 -0
  17. package/dist/head.d.ts +54 -0
  18. package/dist/head.js +114 -0
  19. package/dist/head.js.map +1 -0
  20. package/dist/headers.d.ts +50 -0
  21. package/dist/headers.js +155 -0
  22. package/dist/headers.js.map +1 -0
  23. package/dist/index.d.ts +40 -9
  24. package/dist/index.js +330 -95
  25. package/dist/index.js.map +1 -1
  26. package/dist/init.d.ts +41 -0
  27. package/dist/init.js +130 -0
  28. package/dist/init.js.map +1 -0
  29. package/dist/names.d.ts +5 -0
  30. package/dist/names.js +5 -0
  31. package/dist/names.js.map +1 -1
  32. package/dist/nav.d.ts +51 -0
  33. package/dist/nav.js +97 -0
  34. package/dist/nav.js.map +1 -0
  35. package/dist/permalinks.d.ts +78 -0
  36. package/dist/permalinks.js +114 -0
  37. package/dist/permalinks.js.map +1 -0
  38. package/dist/project.d.ts +37 -0
  39. package/dist/project.js +47 -2
  40. package/dist/project.js.map +1 -1
  41. package/dist/projects.d.ts +38 -0
  42. package/dist/projects.js +127 -0
  43. package/dist/projects.js.map +1 -0
  44. package/dist/records.d.ts +136 -0
  45. package/dist/records.js +182 -0
  46. package/dist/records.js.map +1 -0
  47. package/dist/review/devfile.d.ts +6 -1
  48. package/dist/review/devfile.js +12 -2
  49. package/dist/review/devfile.js.map +1 -1
  50. package/dist/review/diff.d.ts +13 -0
  51. package/dist/review/diff.js +34 -1
  52. package/dist/review/diff.js.map +1 -1
  53. package/dist/review/protocol.d.ts +6 -1
  54. package/dist/review/protocol.js.map +1 -1
  55. package/dist/review/routes.d.ts +3 -0
  56. package/dist/review/routes.js +49 -35
  57. package/dist/review/routes.js.map +1 -1
  58. package/dist/review/server.d.ts +15 -0
  59. package/dist/review/server.js +58 -7
  60. package/dist/review/server.js.map +1 -1
  61. package/dist/root-files.d.ts +68 -0
  62. package/dist/root-files.js +103 -0
  63. package/dist/root-files.js.map +1 -0
  64. package/dist/run.d.ts +7 -6
  65. package/dist/run.js +14 -6
  66. package/dist/run.js.map +1 -1
  67. package/dist/shapes.d.ts +8 -1
  68. package/dist/shapes.js.map +1 -1
  69. package/dist/toolbar/app.js +98 -0
  70. package/dist/toolbar/app.js.map +1 -1
  71. package/dist/toolbar/style.d.ts +1 -1
  72. package/dist/toolbar/style.js +31 -23
  73. package/dist/toolbar/style.js.map +1 -1
  74. package/package.json +32 -9
  75. package/src/Head.astro +58 -0
  76. package/src/MissingPermalink.astro +59 -0
  77. package/src/Permalink.astro +78 -0
  78. package/starter/src/content.config.ts +8 -0
  79. package/starter/src/layouts/Docs.astro +87 -0
  80. package/starter/src/nav.ts +13 -0
  81. package/starter/src/pages/[...slug].astro +20 -0
@@ -0,0 +1,136 @@
1
+ /** How a page is treated: listed, listed and not to be quoted, or not listed. */
2
+ export type Listing = "listed" | "unquoted" | "unlisted";
3
+ /**
4
+ * Where a page's listing comes from: `default` when nothing sets it, `page`
5
+ * for the page's own, and for a rule of `[listing]` its level (`site`,
6
+ * `folder`, or `type`), its folder or type, the rule as written, and its line
7
+ * in `ascribe.toml`.
8
+ */
9
+ export interface ListingFrom {
10
+ level: "default" | "page" | "site" | "folder" | "type";
11
+ folder?: string;
12
+ type?: string;
13
+ rule?: string;
14
+ line?: number;
15
+ }
16
+ /** One page's record, with the route it's keyed by. */
17
+ export interface PageRecord {
18
+ /** The page's route, from the host's root. */
19
+ route: string;
20
+ /** The page's path from the content root, with `/`. */
21
+ source: string;
22
+ /** The page's title, as plain text. */
23
+ title?: string;
24
+ /** The page's description, as plain text on one line. */
25
+ description?: string;
26
+ /** The page's own address, absolute, when the project sets `[consumer] site`. */
27
+ canonical?: string;
28
+ listing: Listing;
29
+ /** Where `listing` comes from: the page, a rule of `[listing]`, or neither. */
30
+ listing_from: ListingFrom;
31
+ /** The address of the page's Markdown version, from the host's root, when the build publishes one. */
32
+ markdown?: string;
33
+ /**
34
+ * The page's place in the site, from the top down to the page itself: the
35
+ * page at each of its route's folders that has one, then the page. Absent
36
+ * in records an earlier Ascribe wrote.
37
+ */
38
+ breadcrumbs?: Step[];
39
+ }
40
+ /** One step of a page's breadcrumbs: a page's route, from the host's root, and its title. */
41
+ export interface Step {
42
+ route: string;
43
+ title: string;
44
+ }
45
+ /** What a kind of crawler is for: gathering training data, building an AI search index, or fetching at a person's request. */
46
+ type Purpose = "training" | "search" | "on-request";
47
+ /** The group of `robots.txt` a disallowed purpose writes. */
48
+ export interface RobotsGroup {
49
+ purpose: Purpose;
50
+ /** Every token of the purpose the binary knows, each a `User-agent` line. */
51
+ user_agents: string[];
52
+ /** The group's rules: `Disallow: /`. */
53
+ lines: string[];
54
+ }
55
+ /** What a project says to crawlers. */
56
+ export interface Robots {
57
+ /** The project's own lines, with paths from the host's root. */
58
+ lines: string[];
59
+ /** What the project says of each purpose it sets. */
60
+ purposes?: Partial<Record<Purpose, "allow" | "disallow">>;
61
+ /** A group for each purpose the project disallows. */
62
+ groups?: RobotsGroup[];
63
+ }
64
+ /** `_ascribe/pages.json`. */
65
+ export interface PageRecords {
66
+ version: number;
67
+ site?: string;
68
+ pages: Record<string, Omit<PageRecord, "route">>;
69
+ robots: Robots;
70
+ }
71
+ /** What one project says to crawlers, by the name of its first entry. */
72
+ export interface ProjectRobots {
73
+ name: string;
74
+ robots: Robots;
75
+ }
76
+ /** The records in a build's site output, or `undefined` when there aren't any. */
77
+ export declare function readRecords(siteRoot: string): PageRecords | undefined;
78
+ /**
79
+ * The record of the page whose source path is `source`, in the build whose
80
+ * site output is at `siteRoot`, or `undefined` when it has none. Read again
81
+ * when the file changes, as it does with each build in `astro dev`.
82
+ */
83
+ export declare function recordFor(siteRoot: string, source: string): PageRecord | undefined;
84
+ /** The records' pages, each with its route, in the file's order. */
85
+ export declare function pageList(records: PageRecords | undefined): PageRecord[];
86
+ /**
87
+ * A route as the sitemap compares it: with a leading `/` and no trailing one,
88
+ * except at the root, so `/docs/` and `/docs` are one page.
89
+ */
90
+ export declare function routeKey(route: string): string;
91
+ /** A route ending the way Astro's `trailingSlash` says, as a built page's address. */
92
+ export declare function routeAs(route: string, trailingSlash: "always" | "never" | "ignore"): string;
93
+ /** What the sitemap is drawn from. */
94
+ export interface SitemapSources {
95
+ /** Every project's records. */
96
+ projects: readonly PageRecord[][];
97
+ /**
98
+ * Whether a project's page has a route in the site: was built, or, in
99
+ * `astro dev`, matches one of the site's routes.
100
+ */
101
+ routed: (route: string) => boolean;
102
+ /** The site's own pages, each by its route from the host's root. */
103
+ sitePages: readonly string[];
104
+ /** Routes from the host's root to leave out: forwarding pages, the not-found page, and the site's list. */
105
+ excluded: readonly string[];
106
+ /** Astro's `site`. */
107
+ site: string;
108
+ }
109
+ /**
110
+ * The sitemap's addresses, absolute, sorted, each once: every listed page of
111
+ * every project that has a route in the site, and every page of the site's
112
+ * own, less the excluded routes. A project's page that isn't listed is left
113
+ * out, and so is a page of the site's own at the same route as one.
114
+ */
115
+ export declare function sitemapAddresses(sources: SitemapSources): string[];
116
+ /** A sitemap of `addresses`, with no dates. */
117
+ export declare function sitemapXml(addresses: readonly string[]): string;
118
+ /**
119
+ * A site's `robots.txt` from what each project says to crawlers: a group for
120
+ * each purpose a project disallows, once for the site, then a
121
+ * `User-agent: *` group with every project's lines before its first
122
+ * `User-agent` line, which are for every crawler, then the groups each
123
+ * project wrote whole, then the sitemap's address. `undefined` when it would
124
+ * say nothing.
125
+ */
126
+ export declare function robotsText(projects: readonly Robots[], sitemap: string | undefined): string | undefined;
127
+ /**
128
+ * Two projects that say contradictory things about one purpose, as
129
+ * sentences: one allows it and the other disallows it. A site has one
130
+ * `robots.txt`, so they must agree. A project that says nothing of a purpose
131
+ * agrees with either.
132
+ */
133
+ export declare function purposeConflicts(projects: readonly ProjectRobots[]): string[];
134
+ /** Where the sitemap is, from the host's root: at Astro's `base`. */
135
+ export declare function sitemapRoute(base: string): string;
136
+ export {};
@@ -0,0 +1,182 @@
1
+ // The page records: `_ascribe/pages.json` in a build's site output
2
+ // (output-layout contract §1.3), what each page tells every reader that isn't
3
+ // a person. The integration writes from them a page's head (`Head.astro`),
4
+ // the site's one sitemap, and its one `robots.txt`, from every project's
5
+ // records together, at the host's root.
6
+ import { readFileSync, statSync } from "node:fs";
7
+ import path from "node:path";
8
+ import { normalizeBase } from "./project.js";
9
+ /** The records' format version this reads. */
10
+ const VERSION = 1;
11
+ function recordsFile(siteRoot) {
12
+ return path.join(siteRoot, "_ascribe", "pages.json");
13
+ }
14
+ /** The records in a build's site output, or `undefined` when there aren't any. */
15
+ export function readRecords(siteRoot) {
16
+ const file = recordsFile(siteRoot);
17
+ let text;
18
+ try {
19
+ text = readFileSync(file, "utf8");
20
+ }
21
+ catch {
22
+ return undefined;
23
+ }
24
+ const records = JSON.parse(text);
25
+ if (records.version !== VERSION) {
26
+ throw new Error(`@ascribed/astro: ${file} is version ${String(records.version)} of the page records, and this version of the integration reads version ${VERSION}. Update @ascribed/astro and @ascribed/cli together.`);
27
+ }
28
+ return records;
29
+ }
30
+ /** A build's records by source path, as last read, and when its file was written. */
31
+ const bySource = new Map();
32
+ /**
33
+ * The record of the page whose source path is `source`, in the build whose
34
+ * site output is at `siteRoot`, or `undefined` when it has none. Read again
35
+ * when the file changes, as it does with each build in `astro dev`.
36
+ */
37
+ export function recordFor(siteRoot, source) {
38
+ let written;
39
+ try {
40
+ written = statSync(recordsFile(siteRoot)).mtimeMs;
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ let known = bySource.get(siteRoot);
46
+ if (known === undefined || known.written !== written) {
47
+ const pages = new Map();
48
+ for (const record of pageList(readRecords(siteRoot))) {
49
+ if (!pages.has(record.source))
50
+ pages.set(record.source, record);
51
+ }
52
+ known = { written, pages };
53
+ bySource.set(siteRoot, known);
54
+ }
55
+ return known.pages.get(source);
56
+ }
57
+ /** The records' pages, each with its route, in the file's order. */
58
+ export function pageList(records) {
59
+ return Object.entries(records?.pages ?? {}).map(([route, page]) => ({ route, ...page }));
60
+ }
61
+ /**
62
+ * A route as the sitemap compares it: with a leading `/` and no trailing one,
63
+ * except at the root, so `/docs/` and `/docs` are one page.
64
+ */
65
+ export function routeKey(route) {
66
+ const withSlash = route.startsWith("/") ? route : `/${route}`;
67
+ return withSlash.length > 1 && withSlash.endsWith("/") ? withSlash.slice(0, -1) : withSlash;
68
+ }
69
+ /** A route ending the way Astro's `trailingSlash` says, as a built page's address. */
70
+ export function routeAs(route, trailingSlash) {
71
+ if (trailingSlash === "never")
72
+ return routeKey(route);
73
+ if (trailingSlash === "always" && !route.endsWith("/"))
74
+ return `${route}/`;
75
+ return route;
76
+ }
77
+ /**
78
+ * The sitemap's addresses, absolute, sorted, each once: every listed page of
79
+ * every project that has a route in the site, and every page of the site's
80
+ * own, less the excluded routes. A project's page that isn't listed is left
81
+ * out, and so is a page of the site's own at the same route as one.
82
+ */
83
+ export function sitemapAddresses(sources) {
84
+ const excluded = new Set(sources.excluded.map(routeKey));
85
+ const records = new Set();
86
+ const routes = new Map();
87
+ for (const pages of sources.projects) {
88
+ for (const page of pages) {
89
+ const key = routeKey(page.route);
90
+ records.add(key);
91
+ if (page.listing === "unlisted" || excluded.has(key) || !sources.routed(page.route))
92
+ continue;
93
+ if (!routes.has(key))
94
+ routes.set(key, page.route);
95
+ }
96
+ }
97
+ for (const route of sources.sitePages) {
98
+ const key = routeKey(route);
99
+ if (records.has(key) || excluded.has(key) || routes.has(key))
100
+ continue;
101
+ routes.set(key, route);
102
+ }
103
+ return [
104
+ ...new Set([...routes.values()].map((route) => new URL(route, sources.site).href)),
105
+ ].sort();
106
+ }
107
+ function escapeXml(text) {
108
+ return text
109
+ .replaceAll("&", "&amp;")
110
+ .replaceAll("<", "&lt;")
111
+ .replaceAll(">", "&gt;")
112
+ .replaceAll('"', "&quot;")
113
+ .replaceAll("'", "&apos;");
114
+ }
115
+ /** A sitemap of `addresses`, with no dates. */
116
+ export function sitemapXml(addresses) {
117
+ const urls = addresses.map((address) => ` <url><loc>${escapeXml(address)}</loc></url>\n`);
118
+ return ('<?xml version="1.0" encoding="UTF-8"?>\n' +
119
+ '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n' +
120
+ urls.join("") +
121
+ "</urlset>\n");
122
+ }
123
+ const USER_AGENT = /^\s*user-agent\s*:/i;
124
+ /**
125
+ * A site's `robots.txt` from what each project says to crawlers: a group for
126
+ * each purpose a project disallows, once for the site, then a
127
+ * `User-agent: *` group with every project's lines before its first
128
+ * `User-agent` line, which are for every crawler, then the groups each
129
+ * project wrote whole, then the sitemap's address. `undefined` when it would
130
+ * say nothing.
131
+ */
132
+ export function robotsText(projects, sitemap) {
133
+ const purposes = new Map();
134
+ const everyone = [];
135
+ const groups = [];
136
+ for (const robots of projects) {
137
+ for (const group of robots.groups ?? []) {
138
+ if (purposes.has(group.purpose))
139
+ continue;
140
+ purposes.set(group.purpose, [
141
+ ...group.user_agents.map((agent) => `User-agent: ${agent}`),
142
+ ...group.lines,
143
+ ]);
144
+ }
145
+ const lines = robots.lines;
146
+ const first = lines.findIndex((line) => USER_AGENT.test(line));
147
+ everyone.push(...(first === -1 ? lines : lines.slice(0, first)));
148
+ if (first !== -1)
149
+ groups.push(lines.slice(first));
150
+ }
151
+ const blocks = [...purposes.values()];
152
+ if (everyone.length > 0)
153
+ blocks.push(["User-agent: *", ...everyone]);
154
+ blocks.push(...groups);
155
+ if (sitemap !== undefined)
156
+ blocks.push([`Sitemap: ${sitemap}`]);
157
+ if (blocks.length === 0)
158
+ return undefined;
159
+ return `${blocks.map((block) => block.join("\n")).join("\n\n")}\n`;
160
+ }
161
+ /**
162
+ * Two projects that say contradictory things about one purpose, as
163
+ * sentences: one allows it and the other disallows it. A site has one
164
+ * `robots.txt`, so they must agree. A project that says nothing of a purpose
165
+ * agrees with either.
166
+ */
167
+ export function purposeConflicts(projects) {
168
+ const problems = [];
169
+ for (const purpose of ["training", "search", "on-request"]) {
170
+ const allow = projects.find((p) => p.robots.purposes?.[purpose] === "allow");
171
+ const disallow = projects.find((p) => p.robots.purposes?.[purpose] === "disallow");
172
+ if (allow === undefined || disallow === undefined)
173
+ continue;
174
+ problems.push(`"${disallow.name}" has [crawlers] ${purpose} = "disallow" and "${allow.name}" has ${purpose} = "allow". The site has one robots.txt, so its projects must agree: make them the same, or remove one.`);
175
+ }
176
+ return problems;
177
+ }
178
+ /** Where the sitemap is, from the host's root: at Astro's `base`. */
179
+ export function sitemapRoute(base) {
180
+ return `${normalizeBase(base)}sitemap.xml`;
181
+ }
182
+ //# sourceMappingURL=records.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"records.js","sourceRoot":"","sources":["../src/records.ts"],"names":[],"mappings":"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","sourcesContent":["// The page records: `_ascribe/pages.json` in a build's site output\n// (output-layout contract §1.3), what each page tells every reader that isn't\n// a person. The integration writes from them a page's head (`Head.astro`),\n// the site's one sitemap, and its one `robots.txt`, from every project's\n// records together, at the host's root.\nimport { readFileSync, statSync } from \"node:fs\";\nimport path from \"node:path\";\nimport { normalizeBase } from \"./project.js\";\n\n/** The records' format version this reads. */\nconst VERSION = 1;\n\n/** How a page is treated: listed, listed and not to be quoted, or not listed. */\nexport type Listing = \"listed\" | \"unquoted\" | \"unlisted\";\n\n/**\n * Where a page's listing comes from: `default` when nothing sets it, `page`\n * for the page's own, and for a rule of `[listing]` its level (`site`,\n * `folder`, or `type`), its folder or type, the rule as written, and its line\n * in `ascribe.toml`.\n */\nexport interface ListingFrom {\n level: \"default\" | \"page\" | \"site\" | \"folder\" | \"type\";\n folder?: string;\n type?: string;\n rule?: string;\n line?: number;\n}\n\n/** One page's record, with the route it's keyed by. */\nexport interface PageRecord {\n /** The page's route, from the host's root. */\n route: string;\n /** The page's path from the content root, with `/`. */\n source: string;\n /** The page's title, as plain text. */\n title?: string;\n /** The page's description, as plain text on one line. */\n description?: string;\n /** The page's own address, absolute, when the project sets `[consumer] site`. */\n canonical?: string;\n listing: Listing;\n /** Where `listing` comes from: the page, a rule of `[listing]`, or neither. */\n listing_from: ListingFrom;\n /** The address of the page's Markdown version, from the host's root, when the build publishes one. */\n markdown?: string;\n /**\n * The page's place in the site, from the top down to the page itself: the\n * page at each of its route's folders that has one, then the page. Absent\n * in records an earlier Ascribe wrote.\n */\n breadcrumbs?: Step[];\n}\n\n/** One step of a page's breadcrumbs: a page's route, from the host's root, and its title. */\nexport interface Step {\n route: string;\n title: string;\n}\n\n/** What a kind of crawler is for: gathering training data, building an AI search index, or fetching at a person's request. */\ntype Purpose = \"training\" | \"search\" | \"on-request\";\n\n/** The group of `robots.txt` a disallowed purpose writes. */\nexport interface RobotsGroup {\n purpose: Purpose;\n /** Every token of the purpose the binary knows, each a `User-agent` line. */\n user_agents: string[];\n /** The group's rules: `Disallow: /`. */\n lines: string[];\n}\n\n/** What a project says to crawlers. */\nexport interface Robots {\n /** The project's own lines, with paths from the host's root. */\n lines: string[];\n /** What the project says of each purpose it sets. */\n purposes?: Partial<Record<Purpose, \"allow\" | \"disallow\">>;\n /** A group for each purpose the project disallows. */\n groups?: RobotsGroup[];\n}\n\n/** `_ascribe/pages.json`. */\nexport interface PageRecords {\n version: number;\n site?: string;\n pages: Record<string, Omit<PageRecord, \"route\">>;\n robots: Robots;\n}\n\n/** What one project says to crawlers, by the name of its first entry. */\nexport interface ProjectRobots {\n name: string;\n robots: Robots;\n}\n\nfunction recordsFile(siteRoot: string): string {\n return path.join(siteRoot, \"_ascribe\", \"pages.json\");\n}\n\n/** The records in a build's site output, or `undefined` when there aren't any. */\nexport function readRecords(siteRoot: string): PageRecords | undefined {\n const file = recordsFile(siteRoot);\n let text: string;\n try {\n text = readFileSync(file, \"utf8\");\n } catch {\n return undefined;\n }\n const records = JSON.parse(text) as PageRecords;\n if (records.version !== VERSION) {\n throw new Error(\n `@ascribed/astro: ${file} is version ${String(records.version)} of the page records, and this version of the integration reads version ${VERSION}. Update @ascribed/astro and @ascribed/cli together.`,\n );\n }\n return records;\n}\n\n/** A build's records by source path, as last read, and when its file was written. */\nconst bySource = new Map<string, { written: number; pages: Map<string, PageRecord> }>();\n\n/**\n * The record of the page whose source path is `source`, in the build whose\n * site output is at `siteRoot`, or `undefined` when it has none. Read again\n * when the file changes, as it does with each build in `astro dev`.\n */\nexport function recordFor(siteRoot: string, source: string): PageRecord | undefined {\n let written: number;\n try {\n written = statSync(recordsFile(siteRoot)).mtimeMs;\n } catch {\n return undefined;\n }\n let known = bySource.get(siteRoot);\n if (known === undefined || known.written !== written) {\n const pages = new Map<string, PageRecord>();\n for (const record of pageList(readRecords(siteRoot))) {\n if (!pages.has(record.source)) pages.set(record.source, record);\n }\n known = { written, pages };\n bySource.set(siteRoot, known);\n }\n return known.pages.get(source);\n}\n\n/** The records' pages, each with its route, in the file's order. */\nexport function pageList(records: PageRecords | undefined): PageRecord[] {\n return Object.entries(records?.pages ?? {}).map(([route, page]) => ({ route, ...page }));\n}\n\n/**\n * A route as the sitemap compares it: with a leading `/` and no trailing one,\n * except at the root, so `/docs/` and `/docs` are one page.\n */\nexport function routeKey(route: string): string {\n const withSlash = route.startsWith(\"/\") ? route : `/${route}`;\n return withSlash.length > 1 && withSlash.endsWith(\"/\") ? withSlash.slice(0, -1) : withSlash;\n}\n\n/** A route ending the way Astro's `trailingSlash` says, as a built page's address. */\nexport function routeAs(route: string, trailingSlash: \"always\" | \"never\" | \"ignore\"): string {\n if (trailingSlash === \"never\") return routeKey(route);\n if (trailingSlash === \"always\" && !route.endsWith(\"/\")) return `${route}/`;\n return route;\n}\n\n/** What the sitemap is drawn from. */\nexport interface SitemapSources {\n /** Every project's records. */\n projects: readonly PageRecord[][];\n /**\n * Whether a project's page has a route in the site: was built, or, in\n * `astro dev`, matches one of the site's routes.\n */\n routed: (route: string) => boolean;\n /** The site's own pages, each by its route from the host's root. */\n sitePages: readonly string[];\n /** Routes from the host's root to leave out: forwarding pages, the not-found page, and the site's list. */\n excluded: readonly string[];\n /** Astro's `site`. */\n site: string;\n}\n\n/**\n * The sitemap's addresses, absolute, sorted, each once: every listed page of\n * every project that has a route in the site, and every page of the site's\n * own, less the excluded routes. A project's page that isn't listed is left\n * out, and so is a page of the site's own at the same route as one.\n */\nexport function sitemapAddresses(sources: SitemapSources): string[] {\n const excluded = new Set(sources.excluded.map(routeKey));\n const records = new Set<string>();\n const routes = new Map<string, string>();\n for (const pages of sources.projects) {\n for (const page of pages) {\n const key = routeKey(page.route);\n records.add(key);\n if (page.listing === \"unlisted\" || excluded.has(key) || !sources.routed(page.route)) continue;\n if (!routes.has(key)) routes.set(key, page.route);\n }\n }\n for (const route of sources.sitePages) {\n const key = routeKey(route);\n if (records.has(key) || excluded.has(key) || routes.has(key)) continue;\n routes.set(key, route);\n }\n return [\n ...new Set([...routes.values()].map((route) => new URL(route, sources.site).href)),\n ].sort();\n}\n\nfunction escapeXml(text: string): string {\n return text\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\")\n .replaceAll(\"'\", \"&apos;\");\n}\n\n/** A sitemap of `addresses`, with no dates. */\nexport function sitemapXml(addresses: readonly string[]): string {\n const urls = addresses.map((address) => ` <url><loc>${escapeXml(address)}</loc></url>\\n`);\n return (\n '<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n' +\n '<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\\n' +\n urls.join(\"\") +\n \"</urlset>\\n\"\n );\n}\n\nconst USER_AGENT = /^\\s*user-agent\\s*:/i;\n\n/**\n * A site's `robots.txt` from what each project says to crawlers: a group for\n * each purpose a project disallows, once for the site, then a\n * `User-agent: *` group with every project's lines before its first\n * `User-agent` line, which are for every crawler, then the groups each\n * project wrote whole, then the sitemap's address. `undefined` when it would\n * say nothing.\n */\nexport function robotsText(\n projects: readonly Robots[],\n sitemap: string | undefined,\n): string | undefined {\n const purposes = new Map<Purpose, string[]>();\n const everyone: string[] = [];\n const groups: string[][] = [];\n for (const robots of projects) {\n for (const group of robots.groups ?? []) {\n if (purposes.has(group.purpose)) continue;\n purposes.set(group.purpose, [\n ...group.user_agents.map((agent) => `User-agent: ${agent}`),\n ...group.lines,\n ]);\n }\n const lines = robots.lines;\n const first = lines.findIndex((line) => USER_AGENT.test(line));\n everyone.push(...(first === -1 ? lines : lines.slice(0, first)));\n if (first !== -1) groups.push(lines.slice(first));\n }\n const blocks: string[][] = [...purposes.values()];\n if (everyone.length > 0) blocks.push([\"User-agent: *\", ...everyone]);\n blocks.push(...groups);\n if (sitemap !== undefined) blocks.push([`Sitemap: ${sitemap}`]);\n if (blocks.length === 0) return undefined;\n return `${blocks.map((block) => block.join(\"\\n\")).join(\"\\n\\n\")}\\n`;\n}\n\n/**\n * Two projects that say contradictory things about one purpose, as\n * sentences: one allows it and the other disallows it. A site has one\n * `robots.txt`, so they must agree. A project that says nothing of a purpose\n * agrees with either.\n */\nexport function purposeConflicts(projects: readonly ProjectRobots[]): string[] {\n const problems: string[] = [];\n for (const purpose of [\"training\", \"search\", \"on-request\"] as const) {\n const allow = projects.find((p) => p.robots.purposes?.[purpose] === \"allow\");\n const disallow = projects.find((p) => p.robots.purposes?.[purpose] === \"disallow\");\n if (allow === undefined || disallow === undefined) continue;\n problems.push(\n `\"${disallow.name}\" has [crawlers] ${purpose} = \"disallow\" and \"${allow.name}\" has ${purpose} = \"allow\". The site has one robots.txt, so its projects must agree: make them the same, or remove one.`,\n );\n }\n return problems;\n}\n\n/** Where the sitemap is, from the host's root: at Astro's `base`. */\nexport function sitemapRoute(base: string): string {\n return `${normalizeBase(base)}sitemap.xml`;\n}\n"]}
@@ -15,7 +15,12 @@ export declare function siteUrl(address: AddressInfo, options: {
15
15
  https: boolean;
16
16
  base: string;
17
17
  }): string;
18
- /** Writes `dev.json`. A failure is the caller's to log: it only costs the editor its link. */
18
+ /**
19
+ * Writes `dev.json`. A failure is the caller's to log: it only costs the editor its link.
20
+ * `.ascribe` or `dev.json` that's a link (a symbolic link, or a junction on Windows, which
21
+ * Node's `lstat` reports as one) is refused, not written through: a committed link could
22
+ * lead anywhere.
23
+ */
19
24
  export declare function writeDevFile(projectDir: string, file: DevFile): void;
20
25
  /** Removes `dev.json` if this process wrote it. */
21
26
  export declare function removeDevFile(projectDir: string, pid: number): void;
@@ -1,7 +1,7 @@
1
1
  // `.ascribe/dev.json`: where the dev server is, for the editor's Open Site
2
2
  // Preview. Written when the server starts, removed when it stops. A server
3
3
  // that crashed leaves it behind, so a reader checks the URL answers first.
4
- import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4
+ import { lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
5
5
  import path from "node:path";
6
6
  /** `<project>/.ascribe/dev.json`. */
7
7
  export function devFilePath(projectDir) {
@@ -17,9 +17,19 @@ export function siteUrl(address, options) {
17
17
  const base = options.base.endsWith("/") ? options.base : `${options.base}/`;
18
18
  return `${options.https ? "https" : "http"}://${host}:${address.port}${base.startsWith("/") ? base : `/${base}`}`;
19
19
  }
20
- /** Writes `dev.json`. A failure is the caller's to log: it only costs the editor its link. */
20
+ /**
21
+ * Writes `dev.json`. A failure is the caller's to log: it only costs the editor its link.
22
+ * `.ascribe` or `dev.json` that's a link (a symbolic link, or a junction on Windows, which
23
+ * Node's `lstat` reports as one) is refused, not written through: a committed link could
24
+ * lead anywhere.
25
+ */
21
26
  export function writeDevFile(projectDir, file) {
22
27
  const target = devFilePath(projectDir);
28
+ for (const at of [path.dirname(target), target]) {
29
+ if (lstatSync(at, { throwIfNoEntry: false })?.isSymbolicLink()) {
30
+ throw new Error(`${at} is a link; replace it with a folder or a file, or remove it`);
31
+ }
32
+ }
23
33
  mkdirSync(path.dirname(target), { recursive: true });
24
34
  writeFileSync(target, `${JSON.stringify(file, null, 2)}\n`);
25
35
  }
@@ -1 +1 @@
1
- {"version":3,"file":"devfile.js","sourceRoot":"","sources":["../../src/review/devfile.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,2EAA2E;AAC3E,2EAA2E;AAC3E,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAEzE,OAAO,IAAI,MAAM,WAAW,CAAC;AAY7B,qCAAqC;AACrC,MAAM,UAAU,WAAW,CAAC,UAAkB;IAC5C,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;AACvD,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,OAAO,CAAC,OAAoB,EAAE,OAAyC;IACrF,IAAI,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;IAC3B,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,IAAI,GAAG,WAAW,CAAC;SAC5E,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,IAAI,GAAG,IAAI,IAAI,GAAG,CAAC;IAChD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,GAAG,CAAC;IAC5E,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,MAAM,IAAI,IAAI,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;AACpH,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,YAAY,CAAC,UAAkB,EAAE,IAAa;IAC5D,MAAM,MAAM,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;IACvC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrD,aAAa,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AAC9D,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,aAAa,CAAC,UAAkB,EAAE,GAAW;IAC3D,MAAM,MAAM,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;IACvC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAqB,CAAC;QAC7E,IAAI,OAAO,CAAC,GAAG,KAAK,GAAG;YAAE,OAAO;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;IACT,CAAC;IACD,MAAM,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAClC,CAAC","sourcesContent":["// `.ascribe/dev.json`: where the dev server is, for the editor's Open Site\n// Preview. Written when the server starts, removed when it stops. A server\n// that crashed leaves it behind, so a reader checks the URL answers first.\nimport { mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport type { AddressInfo } from \"node:net\";\nimport path from \"node:path\";\n\n/** What `dev.json` holds. */\nexport interface DevFile {\n /** The site's address: origin and base path, `http://localhost:4321/docs/`. */\n url: string;\n /** The build the dev server shows. */\n build: string;\n /** The dev server's process, so another server's file isn't removed. */\n pid: number;\n}\n\n/** `<project>/.ascribe/dev.json`. */\nexport function devFilePath(projectDir: string): string {\n return path.join(projectDir, \".ascribe\", \"dev.json\");\n}\n\n/** The site's address from the server's: a wildcard or loopback host is `localhost`. */\nexport function siteUrl(address: AddressInfo, options: { https: boolean; base: string }): string {\n let host = address.address;\n if ([\"::\", \"0.0.0.0\", \"::1\", \"\", \"localhost\"].includes(host)) host = \"localhost\";\n else if (host.includes(\":\")) host = `[${host}]`;\n const base = options.base.endsWith(\"/\") ? options.base : `${options.base}/`;\n return `${options.https ? \"https\" : \"http\"}://${host}:${address.port}${base.startsWith(\"/\") ? base : `/${base}`}`;\n}\n\n/** Writes `dev.json`. A failure is the caller's to log: it only costs the editor its link. */\nexport function writeDevFile(projectDir: string, file: DevFile): void {\n const target = devFilePath(projectDir);\n mkdirSync(path.dirname(target), { recursive: true });\n writeFileSync(target, `${JSON.stringify(file, null, 2)}\\n`);\n}\n\n/** Removes `dev.json` if this process wrote it. */\nexport function removeDevFile(projectDir: string, pid: number): void {\n const target = devFilePath(projectDir);\n try {\n const current = JSON.parse(readFileSync(target, \"utf8\")) as Partial<DevFile>;\n if (current.pid !== pid) return;\n } catch {\n return;\n }\n rmSync(target, { force: true });\n}\n"]}
1
+ {"version":3,"file":"devfile.js","sourceRoot":"","sources":["../../src/review/devfile.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,2EAA2E;AAC3E,2EAA2E;AAC3E,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAEpF,OAAO,IAAI,MAAM,WAAW,CAAC;AAY7B,qCAAqC;AACrC,MAAM,UAAU,WAAW,CAAC,UAAkB;IAC5C,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,EAAE,UAAU,CAAC,CAAC;AACvD,CAAC;AAED,wFAAwF;AACxF,MAAM,UAAU,OAAO,CAAC,OAAoB,EAAE,OAAyC;IACrF,IAAI,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC;IAC3B,IAAI,CAAC,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAE,WAAW,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;QAAE,IAAI,GAAG,WAAW,CAAC;SAC5E,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,IAAI,GAAG,IAAI,IAAI,GAAG,CAAC;IAChD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,GAAG,CAAC;IAC5E,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,MAAM,IAAI,IAAI,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,EAAE,CAAC;AACpH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,UAAkB,EAAE,IAAa;IAC5D,MAAM,MAAM,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;IACvC,KAAK,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC;QAChD,IAAI,SAAS,CAAC,EAAE,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,EAAE,cAAc,EAAE,EAAE,CAAC;YAC/D,MAAM,IAAI,KAAK,CAAC,GAAG,EAAE,8DAA8D,CAAC,CAAC;QACvF,CAAC;IACH,CAAC;IACD,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrD,aAAa,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;AAC9D,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,aAAa,CAAC,UAAkB,EAAE,GAAW;IAC3D,MAAM,MAAM,GAAG,WAAW,CAAC,UAAU,CAAC,CAAC;IACvC,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAAqB,CAAC;QAC7E,IAAI,OAAO,CAAC,GAAG,KAAK,GAAG;YAAE,OAAO;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;IACT,CAAC;IACD,MAAM,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;AAClC,CAAC","sourcesContent":["// `.ascribe/dev.json`: where the dev server is, for the editor's Open Site\n// Preview. Written when the server starts, removed when it stops. A server\n// that crashed leaves it behind, so a reader checks the URL answers first.\nimport { lstatSync, mkdirSync, readFileSync, rmSync, writeFileSync } from \"node:fs\";\nimport type { AddressInfo } from \"node:net\";\nimport path from \"node:path\";\n\n/** What `dev.json` holds. */\nexport interface DevFile {\n /** The site's address: origin and base path, `http://localhost:4321/docs/`. */\n url: string;\n /** The build the dev server shows. */\n build: string;\n /** The dev server's process, so another server's file isn't removed. */\n pid: number;\n}\n\n/** `<project>/.ascribe/dev.json`. */\nexport function devFilePath(projectDir: string): string {\n return path.join(projectDir, \".ascribe\", \"dev.json\");\n}\n\n/** The site's address from the server's: a wildcard or loopback host is `localhost`. */\nexport function siteUrl(address: AddressInfo, options: { https: boolean; base: string }): string {\n let host = address.address;\n if ([\"::\", \"0.0.0.0\", \"::1\", \"\", \"localhost\"].includes(host)) host = \"localhost\";\n else if (host.includes(\":\")) host = `[${host}]`;\n const base = options.base.endsWith(\"/\") ? options.base : `${options.base}/`;\n return `${options.https ? \"https\" : \"http\"}://${host}:${address.port}${base.startsWith(\"/\") ? base : `/${base}`}`;\n}\n\n/**\n * Writes `dev.json`. A failure is the caller's to log: it only costs the editor its link.\n * `.ascribe` or `dev.json` that's a link (a symbolic link, or a junction on Windows, which\n * Node's `lstat` reports as one) is refused, not written through: a committed link could\n * lead anywhere.\n */\nexport function writeDevFile(projectDir: string, file: DevFile): void {\n const target = devFilePath(projectDir);\n for (const at of [path.dirname(target), target]) {\n if (lstatSync(at, { throwIfNoEntry: false })?.isSymbolicLink()) {\n throw new Error(`${at} is a link; replace it with a folder or a file, or remove it`);\n }\n }\n mkdirSync(path.dirname(target), { recursive: true });\n writeFileSync(target, `${JSON.stringify(file, null, 2)}\\n`);\n}\n\n/** Removes `dev.json` if this process wrote it. */\nexport function removeDevFile(projectDir: string, pid: number): void {\n const target = devFilePath(projectDir);\n try {\n const current = JSON.parse(readFileSync(target, \"utf8\")) as Partial<DevFile>;\n if (current.pid !== pid) return;\n } catch {\n return;\n }\n rmSync(target, { force: true });\n}\n"]}
@@ -19,3 +19,16 @@ export declare function runDiff(options: {
19
19
  }): Promise<DiffResult>;
20
20
  /** The build's part of `ascribe diff --format json`'s output. */
21
21
  export declare function parseDiff(json: string, build: string): DiffResult;
22
+ /**
23
+ * `ascribe diff --format prompt`'s prompt about a page, or a fragment's reach,
24
+ * at a content path; `undefined` when it didn't change. Rejects with
25
+ * `ascribe diff`'s reason as the message.
26
+ */
27
+ export declare function runPrompt(options: {
28
+ binary: string;
29
+ configPath: string;
30
+ build: string;
31
+ cwd: string;
32
+ base: string | undefined;
33
+ path: string;
34
+ }): Promise<string | undefined>;
@@ -1,4 +1,5 @@
1
- // Running `ascribe diff --format json` for the integration's build.
1
+ // Running `ascribe diff` for the integration's build: its changes as JSON, and
2
+ // its agent prompts.
2
3
  import { execFile } from "node:child_process";
3
4
  /**
4
5
  * Compares the build's pages with `base` (the default branch when it's
@@ -45,4 +46,36 @@ export function parseDiff(json, build) {
45
46
  const errors = typeof report.working_tree_errors === "number" ? report.working_tree_errors : 0;
46
47
  return { base: report.base, pages, errors };
47
48
  }
49
+ /**
50
+ * `ascribe diff --format prompt`'s prompt about a page, or a fragment's reach,
51
+ * at a content path; `undefined` when it didn't change. Rejects with
52
+ * `ascribe diff`'s reason as the message.
53
+ */
54
+ export function runPrompt(options) {
55
+ const args = [
56
+ "diff",
57
+ "--format",
58
+ "prompt",
59
+ "--build",
60
+ options.build,
61
+ "--config",
62
+ options.configPath,
63
+ "--color",
64
+ "never",
65
+ ];
66
+ if (options.base !== undefined)
67
+ args.push("--base", options.base);
68
+ // After `--`: a content path that starts with `-` is still a path.
69
+ args.push("--", options.path);
70
+ return new Promise((resolve, reject) => {
71
+ execFile(options.binary, args, { cwd: options.cwd, maxBuffer: 16 * 1024 * 1024, windowsHide: true }, (error, stdout, stderr) => {
72
+ if (error !== null) {
73
+ const reason = stderr.trim();
74
+ reject(new Error(reason === "" ? error.message : reason, { cause: error }));
75
+ return;
76
+ }
77
+ resolve(stdout === "" ? undefined : stdout);
78
+ });
79
+ });
80
+ }
48
81
  //# sourceMappingURL=diff.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/review/diff.ts"],"names":[],"mappings":"AAAA,oEAAoE;AACpE,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAY9C;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,OAMvB;IACC,MAAM,IAAI,GAAG;QACX,MAAM;QACN,UAAU;QACV,MAAM;QACN,SAAS;QACT,OAAO,CAAC,KAAK;QACb,UAAU;QACV,OAAO,CAAC,UAAU;QAClB,SAAS;QACT,OAAO;KACR,CAAC;IACF,IAAI,OAAO,CAAC,IAAI,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,QAAQ,CACN,OAAO,CAAC,MAAM,EACd,IAAI,EACJ,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,SAAS,EAAE,GAAG,GAAG,IAAI,GAAG,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,EACrE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;YACxB,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;gBACnB,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC;gBAC7B,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;gBAC5E,OAAO;YACT,CAAC;YACD,IAAI,CAAC;gBACH,OAAO,CAAC,SAAS,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;YAC5C,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,CAAC,IAAI,KAAK,CAAC,kDAAkD,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;YACnF,CAAC;QACH,CAAC,CACF,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,SAAS,CAAC,IAAY,EAAE,KAAa;IACnD,2DAA2D;IAC3D,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAwB,CAAC;IACvD,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;QAC/D,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;IACvC,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,KAAK,CAAC,EAAE,KAAK,IAAI,EAAE,CAAC;IACxE,MAAM,MAAM,GAAG,OAAO,MAAM,CAAC,mBAAmB,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/F,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AAC9C,CAAC","sourcesContent":["// Running `ascribe diff --format json` for the integration's build.\nimport { execFile } from \"node:child_process\";\nimport type { DiffReport } from \"../shapes.js\";\nimport type { BaseInfo, PageDiff } from \"./protocol.js\";\n\n/** One build's changes against a base. */\nexport interface DiffResult {\n base: BaseInfo;\n pages: PageDiff[];\n /** How many errors `ascribe check` finds in the working tree for the build. */\n errors: number;\n}\n\n/**\n * Compares the build's pages with `base` (the default branch when it's\n * `undefined`). Rejects with `ascribe diff`'s reason as the message.\n */\nexport function runDiff(options: {\n binary: string;\n configPath: string;\n build: string;\n cwd: string;\n base: string | undefined;\n}): Promise<DiffResult> {\n const args = [\n \"diff\",\n \"--format\",\n \"json\",\n \"--build\",\n options.build,\n \"--config\",\n options.configPath,\n \"--color\",\n \"never\",\n ];\n if (options.base !== undefined) args.push(\"--base\", options.base);\n return new Promise((resolve, reject) => {\n execFile(\n options.binary,\n args,\n { cwd: options.cwd, maxBuffer: 256 * 1024 * 1024, windowsHide: true },\n (error, stdout, stderr) => {\n if (error !== null) {\n const reason = stderr.trim();\n reject(new Error(reason === \"\" ? error.message : reason, { cause: error }));\n return;\n }\n try {\n resolve(parseDiff(stdout, options.build));\n } catch (cause) {\n reject(new Error(\"`ascribe diff` wrote output that isn't its JSON.\", { cause }));\n }\n },\n );\n });\n}\n\n/** The build's part of `ascribe diff --format json`'s output. */\nexport function parseDiff(json: string, build: string): DiffResult {\n // Checked as far as telling a diff report from other JSON.\n const report = JSON.parse(json) as Partial<DiffReport>;\n if (report.base === undefined || !Array.isArray(report.builds)) {\n throw new Error(\"not a diff report\");\n }\n const pages = report.builds.find((b) => b.build === build)?.pages ?? [];\n const errors = typeof report.working_tree_errors === \"number\" ? report.working_tree_errors : 0;\n return { base: report.base, pages, errors };\n}\n"]}
1
+ {"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/review/diff.ts"],"names":[],"mappings":"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","sourcesContent":["// Running `ascribe diff` for the integration's build: its changes as JSON, and\n// its agent prompts.\nimport { execFile } from \"node:child_process\";\nimport type { DiffReport } from \"../shapes.js\";\nimport type { BaseInfo, PageDiff } from \"./protocol.js\";\n\n/** One build's changes against a base. */\nexport interface DiffResult {\n base: BaseInfo;\n pages: PageDiff[];\n /** How many errors `ascribe check` finds in the working tree for the build. */\n errors: number;\n}\n\n/**\n * Compares the build's pages with `base` (the default branch when it's\n * `undefined`). Rejects with `ascribe diff`'s reason as the message.\n */\nexport function runDiff(options: {\n binary: string;\n configPath: string;\n build: string;\n cwd: string;\n base: string | undefined;\n}): Promise<DiffResult> {\n const args = [\n \"diff\",\n \"--format\",\n \"json\",\n \"--build\",\n options.build,\n \"--config\",\n options.configPath,\n \"--color\",\n \"never\",\n ];\n if (options.base !== undefined) args.push(\"--base\", options.base);\n return new Promise((resolve, reject) => {\n execFile(\n options.binary,\n args,\n { cwd: options.cwd, maxBuffer: 256 * 1024 * 1024, windowsHide: true },\n (error, stdout, stderr) => {\n if (error !== null) {\n const reason = stderr.trim();\n reject(new Error(reason === \"\" ? error.message : reason, { cause: error }));\n return;\n }\n try {\n resolve(parseDiff(stdout, options.build));\n } catch (cause) {\n reject(new Error(\"`ascribe diff` wrote output that isn't its JSON.\", { cause }));\n }\n },\n );\n });\n}\n\n/** The build's part of `ascribe diff --format json`'s output. */\nexport function parseDiff(json: string, build: string): DiffResult {\n // Checked as far as telling a diff report from other JSON.\n const report = JSON.parse(json) as Partial<DiffReport>;\n if (report.base === undefined || !Array.isArray(report.builds)) {\n throw new Error(\"not a diff report\");\n }\n const pages = report.builds.find((b) => b.build === build)?.pages ?? [];\n const errors = typeof report.working_tree_errors === \"number\" ? report.working_tree_errors : 0;\n return { base: report.base, pages, errors };\n}\n\n/**\n * `ascribe diff --format prompt`'s prompt about a page, or a fragment's reach,\n * at a content path; `undefined` when it didn't change. Rejects with\n * `ascribe diff`'s reason as the message.\n */\nexport function runPrompt(options: {\n binary: string;\n configPath: string;\n build: string;\n cwd: string;\n base: string | undefined;\n path: string;\n}): Promise<string | undefined> {\n const args = [\n \"diff\",\n \"--format\",\n \"prompt\",\n \"--build\",\n options.build,\n \"--config\",\n options.configPath,\n \"--color\",\n \"never\",\n ];\n if (options.base !== undefined) args.push(\"--base\", options.base);\n // After `--`: a content path that starts with `-` is still a path.\n args.push(\"--\", options.path);\n return new Promise((resolve, reject) => {\n execFile(\n options.binary,\n args,\n { cwd: options.cwd, maxBuffer: 16 * 1024 * 1024, windowsHide: true },\n (error, stdout, stderr) => {\n if (error !== null) {\n const reason = stderr.trim();\n reject(new Error(reason === \"\" ? error.message : reason, { cause: error }));\n return;\n }\n resolve(stdout === \"\" ? undefined : stdout);\n },\n );\n });\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import type { BaseInfo, PageDiff } from "../shapes.js";
2
+ import type { FormattedPiece } from "@ascribed/review/overlay";
2
3
  /** `ascribe diff --format json`'s types: a changed page, its block changes, and the base. */
3
4
  export type { BaseInfo, Change, Counts, PageDiff } from "../shapes.js";
4
5
  /** The toolbar app's id, also the prefix of its events. */
@@ -22,9 +23,13 @@ export interface Result {
22
23
  code?: string;
23
24
  };
24
25
  }
25
- /** A changed page in the list: without its changes, and with its title when the build has it. */
26
+ /**
27
+ * A changed page in the list: without its changes, and with its title when
28
+ * the build has it, formatted when its field sets `inline = "code"`.
29
+ */
26
30
  export type ChangedPage = Omit<PageDiff, "changes"> & {
27
31
  title: string | null;
32
+ formatted_title: FormattedPiece[] | null;
28
33
  };
29
34
  /** How the checkout's `HEAD` relates to the pull request's head commit. */
30
35
  export type LocalState = "same" | "behind" | "ahead" | "diverged" | "missing";
@@ -1 +1 @@
1
- {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/review/protocol.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,wEAAwE;AACxE,EAAE;AACF,8EAA8E;AAC9E,yEAAyE;AACzE,8EAA8E;AAC9E,wEAAwE;AACxE,mCAAmC;AAOnC,2DAA2D;AAC3D,MAAM,CAAC,MAAM,MAAM,GAAG,gBAAgB,CAAC;AACvC,MAAM,CAAC,MAAM,aAAa,GAAG,wBAAwB,CAAC;AACtD,MAAM,CAAC,MAAM,YAAY,GAAG,uBAAuB,CAAC;AACpD,MAAM,CAAC,MAAM,aAAa,GAAG,wBAAwB,CAAC","sourcesContent":["// The messages between the toolbar app (in the page) and the dev server,\n// over Astro's dev toolbar channel. Types only: both sides import them.\n//\n// The page sends `ascribe:review:request` with `{ tab, id, method, params }`;\n// the server answers that page with `ascribe:review:result` and the same\n// `tab` and `id`. When a page's request changes the threads, or review stops,\n// the server tells every page with `ascribe:review:changed`, naming the\n// `from` tab, which already knows.\n\nimport type { BaseInfo, PageDiff } from \"../shapes.js\";\n\n/** `ascribe diff --format json`'s types: a changed page, its block changes, and the base. */\nexport type { BaseInfo, Change, Counts, PageDiff } from \"../shapes.js\";\n\n/** The toolbar app's id, also the prefix of its events. */\nexport const APP_ID = \"ascribe:review\";\nexport const REQUEST_EVENT = \"ascribe:review:request\";\nexport const RESULT_EVENT = \"ascribe:review:result\";\nexport const CHANGED_EVENT = \"ascribe:review:changed\";\n\nexport interface Request {\n /** The page's own id, made when it loads. */\n tab: string;\n id: number;\n method: string;\n params: Record<string, unknown>;\n}\n\nexport interface Result {\n tab: string;\n id: number;\n result?: unknown;\n error?: { message: string; code?: string };\n}\n\n/** A changed page in the list: without its changes, and with its title when the build has it. */\nexport type ChangedPage = Omit<PageDiff, \"changes\"> & { title: string | null };\n\n/** How the checkout's `HEAD` relates to the pull request's head commit. */\nexport type LocalState = \"same\" | \"behind\" | \"ahead\" | \"diverged\" | \"missing\";\n\n/** The review threads: on, or why not, with a sentence to show. */\nexport type ThreadsState =\n | {\n state: \"on\";\n pullRequest: { number: number; url: string; baseRefName: string };\n local: { state: LocalState; behind: number; ahead: number };\n }\n /** No pull request, no GitHub CLI, no sign-in, a channel other pages could use, or a failure. */\n | {\n state: \"no-pull-request\" | \"gh-missing\" | \"signed-out\" | \"unprotected\" | \"error\";\n message: string;\n };\n\n/** `status`'s answer. */\nexport interface Status {\n on: boolean;\n}\n\n/** `page`'s answer: what the app shows for the page at a route. */\nexport interface PageView {\n /** `\"page\"` for a page of the Ascribe build, `\"not-page\"` for the site's own routes. */\n kind: \"page\" | \"not-page\";\n /** The page's content path; `null` for a route that isn't one. */\n path: string | null;\n /** The page's changes; `null` when it didn't change. */\n page: PageDiff | null;\n base: BaseInfo | null;\n /** Why the changes couldn't be read, when they couldn't. */\n problem: string | null;\n /** How many errors `ascribe check` finds in the working tree for the build. */\n errors: number;\n threads: ThreadsState;\n /** The build's changed pages, in path order. */\n changedPages: ChangedPage[];\n /** The content root, absolute, and the platform's path separator: for opening a source file. */\n contentRoot: string;\n separator: string;\n}\n"]}
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/review/protocol.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,wEAAwE;AACxE,EAAE;AACF,8EAA8E;AAC9E,yEAAyE;AACzE,8EAA8E;AAC9E,wEAAwE;AACxE,mCAAmC;AAQnC,2DAA2D;AAC3D,MAAM,CAAC,MAAM,MAAM,GAAG,gBAAgB,CAAC;AACvC,MAAM,CAAC,MAAM,aAAa,GAAG,wBAAwB,CAAC;AACtD,MAAM,CAAC,MAAM,YAAY,GAAG,uBAAuB,CAAC;AACpD,MAAM,CAAC,MAAM,aAAa,GAAG,wBAAwB,CAAC","sourcesContent":["// The messages between the toolbar app (in the page) and the dev server,\n// over Astro's dev toolbar channel. Types only: both sides import them.\n//\n// The page sends `ascribe:review:request` with `{ tab, id, method, params }`;\n// the server answers that page with `ascribe:review:result` and the same\n// `tab` and `id`. When a page's request changes the threads, or review stops,\n// the server tells every page with `ascribe:review:changed`, naming the\n// `from` tab, which already knows.\n\nimport type { BaseInfo, PageDiff } from \"../shapes.js\";\nimport type { FormattedPiece } from \"@ascribed/review/overlay\";\n\n/** `ascribe diff --format json`'s types: a changed page, its block changes, and the base. */\nexport type { BaseInfo, Change, Counts, PageDiff } from \"../shapes.js\";\n\n/** The toolbar app's id, also the prefix of its events. */\nexport const APP_ID = \"ascribe:review\";\nexport const REQUEST_EVENT = \"ascribe:review:request\";\nexport const RESULT_EVENT = \"ascribe:review:result\";\nexport const CHANGED_EVENT = \"ascribe:review:changed\";\n\nexport interface Request {\n /** The page's own id, made when it loads. */\n tab: string;\n id: number;\n method: string;\n params: Record<string, unknown>;\n}\n\nexport interface Result {\n tab: string;\n id: number;\n result?: unknown;\n error?: { message: string; code?: string };\n}\n\n/**\n * A changed page in the list: without its changes, and with its title when\n * the build has it, formatted when its field sets `inline = \"code\"`.\n */\nexport type ChangedPage = Omit<PageDiff, \"changes\"> & {\n title: string | null;\n formatted_title: FormattedPiece[] | null;\n};\n\n/** How the checkout's `HEAD` relates to the pull request's head commit. */\nexport type LocalState = \"same\" | \"behind\" | \"ahead\" | \"diverged\" | \"missing\";\n\n/** The review threads: on, or why not, with a sentence to show. */\nexport type ThreadsState =\n | {\n state: \"on\";\n pullRequest: { number: number; url: string; baseRefName: string };\n local: { state: LocalState; behind: number; ahead: number };\n }\n /** No pull request, no GitHub CLI, no sign-in, a channel other pages could use, or a failure. */\n | {\n state: \"no-pull-request\" | \"gh-missing\" | \"signed-out\" | \"unprotected\" | \"error\";\n message: string;\n };\n\n/** `status`'s answer. */\nexport interface Status {\n on: boolean;\n}\n\n/** `page`'s answer: what the app shows for the page at a route. */\nexport interface PageView {\n /** `\"page\"` for a page of the Ascribe build, `\"not-page\"` for the site's own routes. */\n kind: \"page\" | \"not-page\";\n /** The page's content path; `null` for a route that isn't one. */\n path: string | null;\n /** The page's changes; `null` when it didn't change. */\n page: PageDiff | null;\n base: BaseInfo | null;\n /** Why the changes couldn't be read, when they couldn't. */\n problem: string | null;\n /** How many errors `ascribe check` finds in the working tree for the build. */\n errors: number;\n threads: ThreadsState;\n /** The build's changed pages, in path order. */\n changedPages: ChangedPage[];\n /** The content root, absolute, and the platform's path separator: for opening a source file. */\n contentRoot: string;\n separator: string;\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import type { FormattedPiece } from "@ascribed/review/overlay";
1
2
  /** A route as compared: decoded, without a trailing `/` (except the root's). */
2
3
  export declare function normalizeRoute(route: string): string;
3
4
  /** A page of the build, as its JSON output names it. */
@@ -5,6 +6,8 @@ export interface RoutedPage {
5
6
  /** The content path. */
6
7
  path: string;
7
8
  title: string | null;
9
+ /** The title formatted, when its field sets `inline = "code"`. */
10
+ formatted_title: FormattedPiece[] | null;
8
11
  }
9
12
  /**
10
13
  * Every page of the build's JSON output (`<output-dir>/<build>/json/`), by