@aeptus/aexeo-emdash 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,119 @@ and the project follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.0] - 2026-05-01
10
+
11
+ Adds upstreamed support for editor-workflow finding suppressions.
12
+ Removes the last reason for consumer sites to maintain a local
13
+ patch over the published plugin.
14
+
15
+ **Compatibility:** verified against emdash `0.7.0` and `0.8.0`.
16
+
17
+ ### Added
18
+
19
+ - **`seogeoPlugin({ suppressions })` option.** Each rule silences
20
+ findings matching the route pattern (glob) AND/OR a rule-id set.
21
+ Applied before findings are persisted to KV — suppressed findings
22
+ never reach the dashboard, /findings, or the per-document panel.
23
+
24
+ ```ts
25
+ seogeoPlugin({
26
+ suppressions: [
27
+ { routePattern: "/privacy", ruleIds: ["RULE001"] },
28
+ { routePattern: "/fr-fr/**", ruleIds: ["RULE002"] },
29
+ { ruleIds: ["RULE999"] }, // applies to every route
30
+ ],
31
+ })
32
+ ```
33
+
34
+ Glob: `*` matches non-`/` chars, `**` matches across `/`, `?`
35
+ matches one non-`/` char. Patterns are anchored. Empty rules
36
+ (`{}` with neither field) are rejected at plugin construction
37
+ with a clear error — that's a kill-switch for all findings
38
+ everywhere and almost certainly an editor mistake.
39
+
40
+ - **`Suppression` type** re-exported from the package root for hosts
41
+ that want to type their config:
42
+
43
+ ```ts
44
+ import type { Suppression } from "@aeptus/aexeo-emdash";
45
+ ```
46
+
47
+ ### Notes for hosts upgrading from 0.3.x
48
+
49
+ - No automatic behavior change. `suppressions` defaults to undefined
50
+ and the filter is a no-op until the host opts in.
51
+ - Suppressions are plugin-only by design. The CLI `check` continues
52
+ to surface every finding; if you want to silence findings at the
53
+ CLI layer, use `seogeo.toml`'s `[ignore]` block. The two surfaces
54
+ serve different audiences: the plugin is editorial workflow, the
55
+ CLI is build-gating.
56
+
57
+ ## [0.3.0] - 2026-05-01
58
+
59
+ Adds an LLM-assisted authoring flow for the truth manifest
60
+ (`facts.json`) — a structured assertion of who the organization is,
61
+ what the products are, and the terminology to use/avoid. AI
62
+ assistants and search crawlers read it for citation grounding;
63
+ authoring it well is a content task, not engineering, but the
64
+ manifest's failure mode (a published manifest with hallucinated
65
+ content) is high-impact.
66
+
67
+ The plugin frames the question, validates the answer, and persists
68
+ the result. The editor's LLM generates. The split is enforced by
69
+ the prompt template, which mandates an interview phase before
70
+ producing JSON: the LLM must ask up to 4 prioritized questions
71
+ (terminology > identity > product/org split > descriptors) so the
72
+ failure mode shifts from confident hallucination to honest gaps.
73
+
74
+ **Compatibility:** verified against emdash `0.7.0` and `0.8.0`.
75
+
76
+ ### Added
77
+
78
+ - **`/facts` admin page.** New entry in the plugin's adminPages.
79
+ Three-section UI: status of stored manifest, generate-prompt
80
+ (copy to clipboard for pasting into the editor's LLM), and
81
+ paste-and-validate (textarea with Validate + Save buttons; Save
82
+ is gated on a clean validation).
83
+ - **Manifest-aware truth scoring.** `scoreIntelligence` now accepts
84
+ an optional manifest argument; the plugin reads it from KV
85
+ (`facts:current`) before scoring and badges the dashboard widget's
86
+ truth stat with the actual signal source — `Truth (manifest+schema)`,
87
+ `Truth (schema only)`, etc. Closes the prior UX-honesty gap where
88
+ the truth score was computed in schema-only mode without telling
89
+ the editor.
90
+ - **`FACTS001` / `FACTS003` findings** on /findings. Surface
91
+ "no manifest authored yet" and "manifest disagrees with on-page
92
+ schema.org" so editors discover the authoring flow through their
93
+ existing surface. Cached under `meta:facts-findings` and
94
+ refreshed on Refresh + on Save, so the data path reads at zero
95
+ WASM cost.
96
+ - **Bridge surface:** `generateFactsPrompt(documents)` and
97
+ `validateFactsManifest(manifest, documents)` WASM exports.
98
+ `scoreIntelligence` gained an optional `manifest_json` parameter.
99
+
100
+ ### Changed
101
+
102
+ - `wasm/aexeo_emdash_bridge_bg.d.ts` is now tracked in git
103
+ (gitignored everything else in `wasm/`). wasm-bindgen 0.2.118
104
+ doesn't emit a `_bg.d.ts` for the bundler target, so this file
105
+ is hand-maintained. The build script snapshots its contents
106
+ before each wasm-bindgen run and restores it (saving the new
107
+ emit under `.wasm-bindgen` for inspection) if a future toolchain
108
+ version begins emitting one — loud warning rather than silent
109
+ regression.
110
+
111
+ ### Notes for hosts upgrading from 0.2.x
112
+
113
+ - A new sidebar entry "Truth manifest" appears under the plugin's
114
+ admin pages. No action required to enable.
115
+ - The dashboard widget's truth-score label now reads
116
+ `Truth (schema only)` until a manifest is authored. Same number,
117
+ more honest framing — no behavioral change.
118
+ - A new sitewide finding (`FACTS001`) appears on /findings until a
119
+ manifest is saved. Severity: warning. Authoring via the new
120
+ /facts page dismisses it.
121
+
9
122
  ## [0.2.0] - 2026-04-28
10
123
 
11
124
  Adds the long-asked clickable routes on the findings page. Block
@@ -153,7 +266,9 @@ First public release.
153
266
  `astro.config.mjs` for the WASM import to resolve to a
154
267
  precompiled `WebAssembly.Module`. Not optional.
155
268
 
156
- [Unreleased]: https://github.com/schiste/Aexeo/compare/aexeo-emdash-v0.2.0...HEAD
269
+ [Unreleased]: https://github.com/schiste/Aexeo/compare/aexeo-emdash-v0.4.0...HEAD
270
+ [0.4.0]: https://github.com/schiste/Aexeo/compare/aexeo-emdash-v0.3.0...aexeo-emdash-v0.4.0
271
+ [0.3.0]: https://github.com/schiste/Aexeo/compare/aexeo-emdash-v0.2.0...aexeo-emdash-v0.3.0
157
272
  [0.2.0]: https://github.com/schiste/Aexeo/compare/aexeo-emdash-v0.1.2...aexeo-emdash-v0.2.0
158
273
  [0.1.2]: https://github.com/schiste/Aexeo/compare/aexeo-emdash-v0.1.1...aexeo-emdash-v0.1.2
159
274
  [0.1.1]: https://github.com/schiste/Aexeo/compare/aexeo-emdash-v0.1.0...aexeo-emdash-v0.1.1
@@ -0,0 +1,3 @@
1
+ import * as React from "react";
2
+ export declare function Facts(): React.JSX.Element;
3
+ //# sourceMappingURL=Facts.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Facts.d.ts","sourceRoot":"","sources":["../../src/admin/Facts.tsx"],"names":[],"mappings":"AAgBA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAsC/B,wBAAgB,KAAK,IAAI,KAAK,CAAC,GAAG,CAAC,OAAO,CAyLzC"}
package/dist/admin.d.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import { Facts } from "./admin/Facts.js";
1
2
  import { Findings } from "./admin/Findings.js";
2
3
  export declare const pages: {
3
4
  "/findings": typeof Findings;
5
+ "/facts": typeof Facts;
4
6
  };
5
7
  //# sourceMappingURL=admin.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../src/admin.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE/C,eAAO,MAAM,KAAK;;CAEjB,CAAC"}
1
+ {"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../src/admin.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACzC,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE/C,eAAO,MAAM,KAAK;;;CAMjB,CAAC"}
package/dist/admin.js CHANGED
@@ -1,20 +1,294 @@
1
- // src/admin/Findings.tsx
1
+ // src/admin/Facts.tsx
2
2
  import { useCallback, useEffect, useState } from "react";
3
- import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
4
  var PLUGIN_ID = "aexeo-seogeo";
5
5
  var API_BASE = "/_emdash/api";
6
+ function Facts() {
7
+ const [data, setData] = useState(null);
8
+ const [loadError, setLoadError] = useState(null);
9
+ const [draft, setDraft] = useState("");
10
+ const [validation, setValidation] = useState(null);
11
+ const [busy, setBusy] = useState(null);
12
+ const [toast, setToast] = useState(null);
13
+ const loadCurrent = useCallback(async () => {
14
+ setLoadError(null);
15
+ try {
16
+ const res = await callRoute("data", {});
17
+ setData(res);
18
+ const stored = res.manifest;
19
+ if (stored !== null) {
20
+ setDraft(JSON.stringify(stored, null, 2));
21
+ }
22
+ } catch (err) {
23
+ setLoadError(err instanceof Error ? err.message : String(err));
24
+ }
25
+ }, []);
26
+ useEffect(() => {
27
+ void loadCurrent();
28
+ }, [loadCurrent]);
29
+ const onCopyPrompt = useCallback(async () => {
30
+ setBusy("prompt");
31
+ setToast(null);
32
+ try {
33
+ const res = await callRoute("prompt", {});
34
+ await navigator.clipboard.writeText(res.prompt);
35
+ setToast(
36
+ "Prompt copied to clipboard \u2014 paste it into Claude/GPT, answer the LLM's questions, then come back and paste the resulting JSON below."
37
+ );
38
+ } catch (err) {
39
+ setToast(
40
+ `Failed to generate prompt: ${err instanceof Error ? err.message : String(err)}`
41
+ );
42
+ } finally {
43
+ setBusy(null);
44
+ }
45
+ }, []);
46
+ const onValidate = useCallback(async () => {
47
+ setBusy("validate");
48
+ setToast(null);
49
+ setValidation(null);
50
+ try {
51
+ const res = await callRoute("validate", {
52
+ manifest_json: draft
53
+ });
54
+ setValidation(res);
55
+ } catch (err) {
56
+ setToast(
57
+ `Validation failed: ${err instanceof Error ? err.message : String(err)}`
58
+ );
59
+ } finally {
60
+ setBusy(null);
61
+ }
62
+ }, [draft]);
63
+ const onSave = useCallback(async () => {
64
+ setBusy("save");
65
+ setToast(null);
66
+ try {
67
+ await callRoute("save", { manifest_json: draft });
68
+ setToast("Manifest saved. Site truth score will reflect it on next refresh.");
69
+ await loadCurrent();
70
+ } catch (err) {
71
+ setToast(
72
+ `Save failed: ${err instanceof Error ? err.message : String(err)}`
73
+ );
74
+ } finally {
75
+ setBusy(null);
76
+ }
77
+ }, [draft, loadCurrent]);
78
+ const validationOk = validation !== null && validation.validation.valid && validation.validation.errors.length === 0 && validation.assessment.mismatches.filter((m) => m.severity === "error").length === 0;
79
+ return /* @__PURE__ */ jsxs("div", { className: "space-y-6 p-4", children: [
80
+ /* @__PURE__ */ jsxs("header", { children: [
81
+ /* @__PURE__ */ jsx("h1", { className: "text-xl font-semibold", children: "Truth manifest" }),
82
+ /* @__PURE__ */ jsxs("p", { className: "mt-1 text-sm text-kumo-subtle", children: [
83
+ "Author a ",
84
+ /* @__PURE__ */ jsx("code", { children: "facts.json" }),
85
+ " using your LLM of choice. The plugin frames the question, validates the answer, and stores the result. Read",
86
+ " ",
87
+ /* @__PURE__ */ jsx(
88
+ "a",
89
+ {
90
+ href: "https://github.com/schiste/Aexeo/blob/main/docs/facts-manifest.md",
91
+ target: "_blank",
92
+ rel: "noopener noreferrer",
93
+ className: "text-kumo-link hover:underline",
94
+ children: "the authoring guide"
95
+ }
96
+ ),
97
+ " ",
98
+ "for the philosophy behind this flow."
99
+ ] })
100
+ ] }),
101
+ loadError !== null && /* @__PURE__ */ jsxs("div", { className: "rounded border border-red-500/40 bg-red-500/10 p-3 text-sm text-red-700", children: [
102
+ "Failed to load manifest state: ",
103
+ loadError
104
+ ] }),
105
+ /* @__PURE__ */ jsxs("section", { className: "rounded border border-kumo-line p-3", children: [
106
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-medium", children: "Status" }),
107
+ /* @__PURE__ */ jsx("p", { className: "mt-2 text-sm", children: data === null ? "Loading\u2026" : data.present ? "\u2713 A truth manifest is stored. Editing the JSON below replaces it on Save." : "No truth manifest stored. Generate the prompt below to author one." })
108
+ ] }),
109
+ /* @__PURE__ */ jsxs("section", { className: "space-y-3 rounded border border-kumo-line p-3", children: [
110
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-medium", children: "1. Generate authoring prompt" }),
111
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-kumo-subtle", children: "Copies a populated prompt to your clipboard. Paste it into Claude/GPT/etc., answer the LLM's questions, and bring the resulting JSON back here." }),
112
+ /* @__PURE__ */ jsx(
113
+ "button",
114
+ {
115
+ type: "button",
116
+ onClick: () => void onCopyPrompt(),
117
+ disabled: busy !== null,
118
+ className: "rounded bg-kumo-brand px-3 py-1.5 text-sm font-medium text-white hover:bg-kumo-brand/90 disabled:opacity-50",
119
+ children: busy === "prompt" ? "Copying\u2026" : "Copy prompt"
120
+ }
121
+ )
122
+ ] }),
123
+ /* @__PURE__ */ jsxs("section", { className: "space-y-3 rounded border border-kumo-line p-3", children: [
124
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-medium", children: "2. Paste & validate" }),
125
+ /* @__PURE__ */ jsx(
126
+ "textarea",
127
+ {
128
+ value: draft,
129
+ onChange: (e) => setDraft(e.target.value),
130
+ spellCheck: false,
131
+ rows: 16,
132
+ placeholder: '{ "version": 1, "organization": { ... }, ... }',
133
+ className: "w-full rounded border border-kumo-line bg-kumo-tint p-2 font-mono text-xs"
134
+ }
135
+ ),
136
+ /* @__PURE__ */ jsxs("div", { className: "flex gap-2", children: [
137
+ /* @__PURE__ */ jsx(
138
+ "button",
139
+ {
140
+ type: "button",
141
+ onClick: () => void onValidate(),
142
+ disabled: busy !== null || draft.trim().length === 0,
143
+ className: "rounded border border-kumo-line px-3 py-1.5 text-sm hover:bg-kumo-tint disabled:opacity-50",
144
+ children: busy === "validate" ? "Validating\u2026" : "Validate"
145
+ }
146
+ ),
147
+ /* @__PURE__ */ jsx(
148
+ "button",
149
+ {
150
+ type: "button",
151
+ onClick: () => void onSave(),
152
+ disabled: busy !== null || !validationOk,
153
+ title: validationOk ? "Save to KV" : "Run Validate first; only valid manifests can be saved.",
154
+ className: "rounded bg-kumo-brand px-3 py-1.5 text-sm font-medium text-white hover:bg-kumo-brand/90 disabled:opacity-50",
155
+ children: busy === "save" ? "Saving\u2026" : "Save"
156
+ }
157
+ )
158
+ ] })
159
+ ] }),
160
+ toast !== null && /* @__PURE__ */ jsx("div", { className: "rounded border border-kumo-line bg-kumo-tint p-3 text-sm", children: toast }),
161
+ validation !== null && /* @__PURE__ */ jsx(ValidationReport, { result: validation })
162
+ ] });
163
+ }
164
+ function ValidationReport({
165
+ result
166
+ }) {
167
+ const errMismatches = result.assessment.mismatches.filter(
168
+ (m) => m.severity === "error"
169
+ );
170
+ const warnMismatches = result.assessment.mismatches.filter(
171
+ (m) => m.severity === "warning"
172
+ );
173
+ return /* @__PURE__ */ jsxs("section", { className: "space-y-3 rounded border border-kumo-line p-3", children: [
174
+ /* @__PURE__ */ jsx("h2", { className: "text-sm font-medium", children: "Validation result" }),
175
+ /* @__PURE__ */ jsxs("div", { className: "text-sm", children: [
176
+ /* @__PURE__ */ jsxs("div", { children: [
177
+ "Shape:",
178
+ " ",
179
+ /* @__PURE__ */ jsx("strong", { children: result.validation.valid ? "valid" : "invalid" }),
180
+ " ",
181
+ "\xB7 org",
182
+ " ",
183
+ result.validation.organization_present ? "present" : "missing",
184
+ " \xB7",
185
+ " ",
186
+ result.validation.product_count,
187
+ " products \xB7",
188
+ " ",
189
+ result.validation.preferred_term_count,
190
+ " preferred terms \xB7",
191
+ " ",
192
+ result.validation.forbidden_term_count,
193
+ " forbidden terms"
194
+ ] }),
195
+ /* @__PURE__ */ jsxs("div", { children: [
196
+ "Score: ",
197
+ /* @__PURE__ */ jsx("strong", { children: result.assessment.score }),
198
+ "/",
199
+ result.assessment.score_ceiling,
200
+ " \xB7 source =",
201
+ " ",
202
+ /* @__PURE__ */ jsx("code", { children: result.assessment.structured_truth_source }),
203
+ " \xB7",
204
+ " ",
205
+ result.assessment.pages_analyzed,
206
+ " pages,",
207
+ " ",
208
+ result.assessment.pages_with_schema,
209
+ " with schema.org"
210
+ ] })
211
+ ] }),
212
+ result.validation.errors.length > 0 && /* @__PURE__ */ jsxs("div", { className: "rounded border border-red-500/40 bg-red-500/10 p-2 text-sm", children: [
213
+ /* @__PURE__ */ jsx("strong", { children: "Schema errors:" }),
214
+ /* @__PURE__ */ jsx("ul", { className: "mt-1 list-disc pl-5", children: result.validation.errors.map((e, i) => /* @__PURE__ */ jsx("li", { children: e }, i)) })
215
+ ] }),
216
+ result.validation.warnings.length > 0 && /* @__PURE__ */ jsxs("div", { className: "rounded border border-yellow-500/40 bg-yellow-500/10 p-2 text-sm", children: [
217
+ /* @__PURE__ */ jsx("strong", { children: "Schema warnings:" }),
218
+ /* @__PURE__ */ jsx("ul", { className: "mt-1 list-disc pl-5", children: result.validation.warnings.map((w, i) => /* @__PURE__ */ jsx("li", { children: w }, i)) })
219
+ ] }),
220
+ errMismatches.length > 0 && /* @__PURE__ */ jsxs("div", { className: "rounded border border-red-500/40 bg-red-500/10 p-2 text-sm", children: [
221
+ /* @__PURE__ */ jsx("strong", { children: "Mismatches with site (errors):" }),
222
+ /* @__PURE__ */ jsx(MismatchList, { items: errMismatches })
223
+ ] }),
224
+ warnMismatches.length > 0 && /* @__PURE__ */ jsxs("div", { className: "rounded border border-yellow-500/40 bg-yellow-500/10 p-2 text-sm", children: [
225
+ /* @__PURE__ */ jsx("strong", { children: "Mismatches with site (warnings):" }),
226
+ /* @__PURE__ */ jsx(MismatchList, { items: warnMismatches })
227
+ ] }),
228
+ result.validation.errors.length === 0 && result.assessment.mismatches.length === 0 && /* @__PURE__ */ jsxs("div", { className: "rounded border border-green-500/40 bg-green-500/10 p-2 text-sm", children: [
229
+ "\u2713 Manifest is valid and agrees with on-page schema.org. Click",
230
+ " ",
231
+ "Save to persist."
232
+ ] })
233
+ ] });
234
+ }
235
+ function MismatchList({
236
+ items
237
+ }) {
238
+ return /* @__PURE__ */ jsx("ul", { className: "mt-1 list-disc pl-5", children: items.map((m, i) => /* @__PURE__ */ jsxs("li", { children: [
239
+ /* @__PURE__ */ jsx("code", { className: "text-xs", children: m.route }),
240
+ " \xB7 ",
241
+ m.field,
242
+ ": expected",
243
+ " ",
244
+ /* @__PURE__ */ jsx("code", { className: "text-xs", children: m.expected }),
245
+ ", observed",
246
+ " ",
247
+ /* @__PURE__ */ jsx("code", { className: "text-xs", children: m.observed }),
248
+ " ",
249
+ /* @__PURE__ */ jsxs("span", { className: "text-xs text-kumo-subtle", children: [
250
+ "(",
251
+ m.source,
252
+ ")"
253
+ ] })
254
+ ] }, i)) });
255
+ }
256
+ async function callRoute(kind, extra) {
257
+ const res = await fetch(`${API_BASE}/plugins/${PLUGIN_ID}/facts`, {
258
+ method: "POST",
259
+ headers: {
260
+ "Content-Type": "application/json",
261
+ "X-EmDash-Request": "1"
262
+ },
263
+ body: JSON.stringify({ kind, ...extra })
264
+ });
265
+ const body = await res.json();
266
+ if (body.error !== void 0) {
267
+ throw new Error(`${body.error.code}: ${body.error.message}`);
268
+ }
269
+ if (body.data === void 0) {
270
+ throw new Error(`Empty response (HTTP ${res.status})`);
271
+ }
272
+ return body.data;
273
+ }
274
+
275
+ // src/admin/Findings.tsx
276
+ import { useCallback as useCallback2, useEffect as useEffect2, useState as useState2 } from "react";
277
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
278
+ var PLUGIN_ID2 = "aexeo-seogeo";
279
+ var API_BASE2 = "/_emdash/api";
6
280
  function Findings() {
7
- const [state, setState] = useState({
281
+ const [state, setState] = useState2({
8
282
  loading: true,
9
283
  error: null,
10
284
  payload: null
11
285
  });
12
- const [refreshing, setRefreshing] = useState(false);
13
- const [toast, setToast] = useState(null);
14
- const load = useCallback(async () => {
286
+ const [refreshing, setRefreshing] = useState2(false);
287
+ const [toast, setToast] = useState2(null);
288
+ const load = useCallback2(async () => {
15
289
  setState((s) => ({ ...s, loading: true, error: null }));
16
290
  try {
17
- const res = await fetch(`${API_BASE}/plugins/${PLUGIN_ID}/data`, {
291
+ const res = await fetch(`${API_BASE2}/plugins/${PLUGIN_ID2}/data`, {
18
292
  method: "POST",
19
293
  headers: csrfHeaders(),
20
294
  body: "{}"
@@ -30,11 +304,11 @@ function Findings() {
30
304
  });
31
305
  }
32
306
  }, []);
33
- const refresh = useCallback(async () => {
307
+ const refresh = useCallback2(async () => {
34
308
  setRefreshing(true);
35
309
  setToast(null);
36
310
  try {
37
- const res = await fetch(`${API_BASE}/plugins/${PLUGIN_ID}/refresh`, {
311
+ const res = await fetch(`${API_BASE2}/plugins/${PLUGIN_ID2}/refresh`, {
38
312
  method: "POST",
39
313
  headers: csrfHeaders(),
40
314
  body: "{}"
@@ -60,20 +334,20 @@ function Findings() {
60
334
  setRefreshing(false);
61
335
  }
62
336
  }, []);
63
- useEffect(() => {
337
+ useEffect2(() => {
64
338
  void load();
65
339
  }, [load]);
66
340
  if (state.loading && state.payload === null) {
67
- return /* @__PURE__ */ jsx("div", { className: "p-4 text-sm text-kumo-subtle", children: "Loading findings\u2026" });
341
+ return /* @__PURE__ */ jsx2("div", { className: "p-4 text-sm text-kumo-subtle", children: "Loading findings\u2026" });
68
342
  }
69
343
  if (state.error !== null) {
70
- return /* @__PURE__ */ jsxs("div", { className: "p-4", children: [
71
- /* @__PURE__ */ jsx("h1", { className: "text-xl font-semibold", children: "SEO findings" }),
72
- /* @__PURE__ */ jsxs("p", { className: "mt-2 rounded border border-red-500/40 bg-red-500/10 p-3 text-sm text-red-700", children: [
344
+ return /* @__PURE__ */ jsxs2("div", { className: "p-4", children: [
345
+ /* @__PURE__ */ jsx2("h1", { className: "text-xl font-semibold", children: "SEO findings" }),
346
+ /* @__PURE__ */ jsxs2("p", { className: "mt-2 rounded border border-red-500/40 bg-red-500/10 p-3 text-sm text-red-700", children: [
73
347
  "Failed to load: ",
74
348
  state.error
75
349
  ] }),
76
- /* @__PURE__ */ jsx(
350
+ /* @__PURE__ */ jsx2(
77
351
  "button",
78
352
  {
79
353
  type: "button",
@@ -86,17 +360,17 @@ function Findings() {
86
360
  }
87
361
  const payload = state.payload;
88
362
  if (payload === null) {
89
- return /* @__PURE__ */ jsx("div", { className: "p-4 text-sm text-kumo-subtle", children: "No data." });
363
+ return /* @__PURE__ */ jsx2("div", { className: "p-4 text-sm text-kumo-subtle", children: "No data." });
90
364
  }
91
365
  const routesByPath = /* @__PURE__ */ new Map();
92
366
  for (const route of payload.routes) {
93
367
  routesByPath.set(route.route, route);
94
368
  }
95
- return /* @__PURE__ */ jsxs("div", { className: "space-y-4 p-4", children: [
96
- /* @__PURE__ */ jsxs("header", { className: "flex items-center justify-between gap-4", children: [
97
- /* @__PURE__ */ jsxs("div", { children: [
98
- /* @__PURE__ */ jsx("h1", { className: "text-xl font-semibold", children: "SEO findings" }),
99
- /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-kumo-subtle", children: payload.totals.findings === 0 ? /* @__PURE__ */ jsx(Fragment, { children: "No findings yet \u2014 click Refresh to evaluate the site." }) : /* @__PURE__ */ jsxs(Fragment, { children: [
369
+ return /* @__PURE__ */ jsxs2("div", { className: "space-y-4 p-4", children: [
370
+ /* @__PURE__ */ jsxs2("header", { className: "flex items-center justify-between gap-4", children: [
371
+ /* @__PURE__ */ jsxs2("div", { children: [
372
+ /* @__PURE__ */ jsx2("h1", { className: "text-xl font-semibold", children: "SEO findings" }),
373
+ /* @__PURE__ */ jsx2("p", { className: "mt-1 text-sm text-kumo-subtle", children: payload.totals.findings === 0 ? /* @__PURE__ */ jsx2(Fragment, { children: "No findings yet \u2014 click Refresh to evaluate the site." }) : /* @__PURE__ */ jsxs2(Fragment, { children: [
100
374
  payload.totals.findings,
101
375
  " findings across ",
102
376
  payload.totals.routes,
@@ -108,7 +382,7 @@ function Findings() {
108
382
  " warnings."
109
383
  ] }) })
110
384
  ] }),
111
- /* @__PURE__ */ jsx(
385
+ /* @__PURE__ */ jsx2(
112
386
  "button",
113
387
  {
114
388
  type: "button",
@@ -119,29 +393,29 @@ function Findings() {
119
393
  }
120
394
  )
121
395
  ] }),
122
- toast !== null && /* @__PURE__ */ jsx("div", { className: "rounded border border-kumo-line bg-kumo-tint p-3 text-sm", children: toast }),
123
- payload.sitewideFindings.length > 0 && /* @__PURE__ */ jsxs("section", { className: "rounded border border-kumo-line p-3", children: [
124
- /* @__PURE__ */ jsx("h2", { className: "text-sm font-medium", children: "Sitewide" }),
125
- /* @__PURE__ */ jsx("ul", { className: "mt-2 space-y-1 text-sm", children: payload.sitewideFindings.map((finding, i) => /* @__PURE__ */ jsxs("li", { className: "flex items-start gap-2", children: [
126
- /* @__PURE__ */ jsx(SeverityBadge, { severity: finding.severity }),
127
- /* @__PURE__ */ jsx("code", { className: "font-mono text-xs", children: finding.rule_id }),
128
- /* @__PURE__ */ jsx("span", { className: "flex-1", children: finding.message })
396
+ toast !== null && /* @__PURE__ */ jsx2("div", { className: "rounded border border-kumo-line bg-kumo-tint p-3 text-sm", children: toast }),
397
+ payload.sitewideFindings.length > 0 && /* @__PURE__ */ jsxs2("section", { className: "rounded border border-kumo-line p-3", children: [
398
+ /* @__PURE__ */ jsx2("h2", { className: "text-sm font-medium", children: "Sitewide" }),
399
+ /* @__PURE__ */ jsx2("ul", { className: "mt-2 space-y-1 text-sm", children: payload.sitewideFindings.map((finding, i) => /* @__PURE__ */ jsxs2("li", { className: "flex items-start gap-2", children: [
400
+ /* @__PURE__ */ jsx2(SeverityBadge, { severity: finding.severity }),
401
+ /* @__PURE__ */ jsx2("code", { className: "font-mono text-xs", children: finding.rule_id }),
402
+ /* @__PURE__ */ jsx2("span", { className: "flex-1", children: finding.message })
129
403
  ] }, i)) })
130
404
  ] }),
131
- payload.findings.length === 0 ? /* @__PURE__ */ jsx("p", { className: "text-sm text-kumo-subtle", children: payload.totals.documentsIndexed === 0 ? "No documents indexed \u2014 click Refresh to do an initial sweep." : "All indexed documents are clean." }) : /* @__PURE__ */ jsx("div", { className: "overflow-x-auto rounded border border-kumo-line", children: /* @__PURE__ */ jsxs("table", { className: "w-full text-left text-sm", children: [
132
- /* @__PURE__ */ jsx("thead", { className: "bg-kumo-tint", children: /* @__PURE__ */ jsxs("tr", { children: [
133
- /* @__PURE__ */ jsx("th", { className: "px-3 py-2 font-medium text-kumo-subtle", children: "Route" }),
134
- /* @__PURE__ */ jsx("th", { className: "px-3 py-2 font-medium text-kumo-subtle", children: "Rule" }),
135
- /* @__PURE__ */ jsx("th", { className: "px-3 py-2 font-medium text-kumo-subtle", children: "Severity" }),
136
- /* @__PURE__ */ jsx("th", { className: "px-3 py-2 font-medium text-kumo-subtle", children: "Message" })
405
+ payload.findings.length === 0 ? /* @__PURE__ */ jsx2("p", { className: "text-sm text-kumo-subtle", children: payload.totals.documentsIndexed === 0 ? "No documents indexed \u2014 click Refresh to do an initial sweep." : "All indexed documents are clean." }) : /* @__PURE__ */ jsx2("div", { className: "overflow-x-auto rounded border border-kumo-line", children: /* @__PURE__ */ jsxs2("table", { className: "w-full text-left text-sm", children: [
406
+ /* @__PURE__ */ jsx2("thead", { className: "bg-kumo-tint", children: /* @__PURE__ */ jsxs2("tr", { children: [
407
+ /* @__PURE__ */ jsx2("th", { className: "px-3 py-2 font-medium text-kumo-subtle", children: "Route" }),
408
+ /* @__PURE__ */ jsx2("th", { className: "px-3 py-2 font-medium text-kumo-subtle", children: "Rule" }),
409
+ /* @__PURE__ */ jsx2("th", { className: "px-3 py-2 font-medium text-kumo-subtle", children: "Severity" }),
410
+ /* @__PURE__ */ jsx2("th", { className: "px-3 py-2 font-medium text-kumo-subtle", children: "Message" })
137
411
  ] }) }),
138
- /* @__PURE__ */ jsx("tbody", { children: payload.findings.map((finding, i) => {
412
+ /* @__PURE__ */ jsx2("tbody", { children: payload.findings.map((finding, i) => {
139
413
  const routeInfo = routesByPath.get(finding.route);
140
- return /* @__PURE__ */ jsxs("tr", { className: "border-t border-kumo-line", children: [
141
- /* @__PURE__ */ jsx("td", { className: "px-3 py-2 align-top", children: /* @__PURE__ */ jsx(RouteLinks, { finding, info: routeInfo }) }),
142
- /* @__PURE__ */ jsx("td", { className: "px-3 py-2 align-top", children: /* @__PURE__ */ jsx("code", { className: "rounded bg-kumo-tint px-1.5 py-0.5 font-mono text-xs", children: finding.rule_id }) }),
143
- /* @__PURE__ */ jsx("td", { className: "px-3 py-2 align-top", children: /* @__PURE__ */ jsx(SeverityBadge, { severity: finding.severity }) }),
144
- /* @__PURE__ */ jsx("td", { className: "px-3 py-2 align-top", children: finding.message })
414
+ return /* @__PURE__ */ jsxs2("tr", { className: "border-t border-kumo-line", children: [
415
+ /* @__PURE__ */ jsx2("td", { className: "px-3 py-2 align-top", children: /* @__PURE__ */ jsx2(RouteLinks, { finding, info: routeInfo }) }),
416
+ /* @__PURE__ */ jsx2("td", { className: "px-3 py-2 align-top", children: /* @__PURE__ */ jsx2("code", { className: "rounded bg-kumo-tint px-1.5 py-0.5 font-mono text-xs", children: finding.rule_id }) }),
417
+ /* @__PURE__ */ jsx2("td", { className: "px-3 py-2 align-top", children: /* @__PURE__ */ jsx2(SeverityBadge, { severity: finding.severity }) }),
418
+ /* @__PURE__ */ jsx2("td", { className: "px-3 py-2 align-top", children: finding.message })
145
419
  ] }, i);
146
420
  }) })
147
421
  ] }) })
@@ -152,18 +426,18 @@ function RouteLinks({
152
426
  info
153
427
  }) {
154
428
  if (info === void 0) {
155
- return /* @__PURE__ */ jsx("span", { className: "font-mono text-xs", children: finding.route });
429
+ return /* @__PURE__ */ jsx2("span", { className: "font-mono text-xs", children: finding.route });
156
430
  }
157
- return /* @__PURE__ */ jsxs("span", { className: "space-x-2", children: [
158
- info.editUrl !== "" ? /* @__PURE__ */ jsx(
431
+ return /* @__PURE__ */ jsxs2("span", { className: "space-x-2", children: [
432
+ info.editUrl !== "" ? /* @__PURE__ */ jsx2(
159
433
  "a",
160
434
  {
161
435
  href: info.editUrl,
162
436
  className: "font-mono text-xs text-kumo-link hover:underline",
163
437
  children: finding.route
164
438
  }
165
- ) : /* @__PURE__ */ jsx("span", { className: "font-mono text-xs", children: finding.route }),
166
- info.publishedUrl !== "" && /* @__PURE__ */ jsx(
439
+ ) : /* @__PURE__ */ jsx2("span", { className: "font-mono text-xs", children: finding.route }),
440
+ info.publishedUrl !== "" && /* @__PURE__ */ jsx2(
167
441
  "a",
168
442
  {
169
443
  href: info.publishedUrl,
@@ -173,12 +447,12 @@ function RouteLinks({
173
447
  children: "live \u2197"
174
448
  }
175
449
  ),
176
- info.status !== "" && info.status !== "published" && /* @__PURE__ */ jsx("span", { className: "text-xs text-kumo-subtle", children: info.status })
450
+ info.status !== "" && info.status !== "published" && /* @__PURE__ */ jsx2("span", { className: "text-xs text-kumo-subtle", children: info.status })
177
451
  ] });
178
452
  }
179
453
  function SeverityBadge({ severity }) {
180
454
  const classes = severity === "error" ? "bg-red-500/10 text-red-700 border-red-500/40" : severity === "warning" ? "bg-yellow-500/10 text-yellow-800 border-yellow-500/40" : "bg-kumo-tint text-kumo-subtle border-kumo-line";
181
- return /* @__PURE__ */ jsx(
455
+ return /* @__PURE__ */ jsx2(
182
456
  "span",
183
457
  {
184
458
  className: `inline-block rounded border px-1.5 py-0.5 text-xs font-medium ${classes}`,
@@ -207,7 +481,11 @@ function unwrap(body, res) {
207
481
 
208
482
  // src/admin.tsx
209
483
  var pages = {
210
- "/findings": Findings
484
+ "/findings": Findings,
485
+ // /facts is the truth-manifest authoring page. Block Kit can't render
486
+ // a textarea or copy-to-clipboard button, so this path goes through
487
+ // the React adminEntry. Same mechanism as /findings.
488
+ "/facts": Facts
211
489
  };
212
490
  export {
213
491
  pages
@@ -1,3 +1,4 @@
1
+ import type { Suppression } from "./suppressions.js";
1
2
  export interface ConfiguredPluginOptions {
2
3
  /**
3
4
  * emdash collections the Refresh button sweeps. Comes through from
@@ -6,6 +7,13 @@ export interface ConfiguredPluginOptions {
6
7
  * (`["posts", "pages"]`).
7
8
  */
8
9
  collections?: readonly string[];
10
+ /**
11
+ * Editor-workflow suppressions. See `Suppression` in suppressions.ts
12
+ * for the rule shape and matching semantics. Compiled once at plugin
13
+ * construction; the resulting filter is captured in the closures
14
+ * attached to the route handlers and the afterSave hook.
15
+ */
16
+ suppressions?: readonly Suppression[];
9
17
  }
10
18
  export declare function createPlugin(options?: ConfiguredPluginOptions): unknown;
11
19
  export default createPlugin;
@@ -1 +1 @@
1
- {"version":3,"file":"configured.d.ts","sourceRoot":"","sources":["../src/configured.ts"],"names":[],"mappings":"AA8EA,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC;AAED,wBAAgB,YAAY,CAAC,OAAO,GAAE,uBAA4B,GAAG,OAAO,CA0E3E;AAID,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"configured.d.ts","sourceRoot":"","sources":["../src/configured.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAoDrD,MAAM,WAAW,uBAAuB;IACtC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAEhC;;;;;OAKG;IACH,YAAY,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;CACvC;AAED,wBAAgB,YAAY,CAAC,OAAO,GAAE,uBAA4B,GAAG,OAAO,CA+F3E;AAID,eAAe,YAAY,CAAC"}