@anthusai/papyrus 4.0.0 → 5.0.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/amplify/functions/editorial-checks/requirements.txt +1 -1
- package/amplify/functions/editorial-checks/resource.js +3 -3
- package/amplify/functions/editorial-checks/resource.ts +3 -3
- package/amplify/functions/editorial-checks/schema.js +2 -2
- package/amplify/functions/editorial-checks/schema.ts +2 -2
- package/amplify/publication/redirect-and-index.js +31 -0
- package/amplify/publication/redirect-rules.js +40 -0
- package/amplify/publication/redirect-rules.ts +47 -0
- package/amplify/publication/s3-projection.js +3 -1
- package/amplify/publication/s3-projection.ts +5 -1
- package/components/editorial-checks-admin-panel.tsx +5 -5
- package/lib/category-repository.ts +10 -8
- package/lib/editorial-checks-demo.ts +1 -1
- package/lib/editorial-checks.ts +2 -2
- package/lib/editorial-preview.ts +5 -5
- package/lib/site-brand-tenant.ts +11 -6
- package/lib/site-brand.ts +6 -3
- package/package.json +1 -1
- package/routes.manifest.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
humanus[jev]>=0.33,<0.34
|
|
@@ -6,10 +6,10 @@ import path from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { pythonLambdaCode } from "../shared/python-bundle.js";
|
|
8
8
|
const functionDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
/** AppSync gives a resolver 30 seconds;
|
|
9
|
+
/** AppSync gives a resolver 30 seconds; Humanus's classifier deadline (settings, default 20 s) keeps a check inside this. */
|
|
10
10
|
export const EDITORIAL_CHECKS_TIMEOUT_SECONDS = 28;
|
|
11
|
-
/**
|
|
12
|
-
export const editorialChecksRequirements = ["
|
|
11
|
+
/** Humanus with the Jev classifier lane; papyrus_content.editorial_checks does the work. */
|
|
12
|
+
export const editorialChecksRequirements = ["humanus[jev]>=0.33,<0.34"];
|
|
13
13
|
function installRequirements(outputDir) {
|
|
14
14
|
execFileSync("python3", [
|
|
15
15
|
"-m", "pip", "install", "-r", path.join(functionDirectory, "requirements.txt"), "-t", outputDir, "--no-cache-dir",
|
|
@@ -9,11 +9,11 @@ import { pythonLambdaCode } from "../shared/python-bundle";
|
|
|
9
9
|
|
|
10
10
|
const functionDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
|
|
12
|
-
/** AppSync gives a resolver 30 seconds;
|
|
12
|
+
/** AppSync gives a resolver 30 seconds; Humanus's classifier deadline (settings, default 20 s) keeps a check inside this. */
|
|
13
13
|
export const EDITORIAL_CHECKS_TIMEOUT_SECONDS = 28;
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
export const editorialChecksRequirements = ["
|
|
15
|
+
/** Humanus with the Jev classifier lane; papyrus_content.editorial_checks does the work. */
|
|
16
|
+
export const editorialChecksRequirements = ["humanus[jev]>=0.33,<0.34"];
|
|
17
17
|
|
|
18
18
|
function installRequirements(outputDir: string): void {
|
|
19
19
|
execFileSync(
|
|
@@ -2,9 +2,9 @@ import { a } from "@aws-amplify/backend";
|
|
|
2
2
|
import { editorialChecks } from "./resource.js";
|
|
3
3
|
const editors = ["editor", "admin"];
|
|
4
4
|
/**
|
|
5
|
-
* Core to every site's data API. Policies, settings and editors' decisions live in Papyrus;
|
|
5
|
+
* Core to every site's data API. Policies, settings and editors' decisions live in Papyrus; Humanus checks
|
|
6
6
|
* content against them. Editors read policies and record decisions; admins
|
|
7
|
-
* manage policies and settings. Only the function (IAM) calls
|
|
7
|
+
* manage policies and settings. Only the function (IAM) calls Humanus.
|
|
8
8
|
*/
|
|
9
9
|
export const editorialChecksSchemaDefinitions = {
|
|
10
10
|
EditorialPolicy: a
|
|
@@ -4,9 +4,9 @@ import { editorialChecks } from "./resource";
|
|
|
4
4
|
const editors = ["editor", "admin"];
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Core to every site's data API. Policies, settings and editors' decisions live in Papyrus;
|
|
7
|
+
* Core to every site's data API. Policies, settings and editors' decisions live in Papyrus; Humanus checks
|
|
8
8
|
* content against them. Editors read policies and record decisions; admins
|
|
9
|
-
* manage policies and settings. Only the function (IAM) calls
|
|
9
|
+
* manage policies and settings. Only the function (IAM) calls Humanus.
|
|
10
10
|
*/
|
|
11
11
|
export const editorialChecksSchemaDefinitions = {
|
|
12
12
|
EditorialPolicy: a
|
|
@@ -22,6 +22,29 @@ function rewriteToIndexDocument(uri) {
|
|
|
22
22
|
return uri;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// Pattern redirects: data from the site's projection.json, spliced between the markers at deploy
|
|
26
|
+
// time by redirect-rules.ts. Each rule is {p: regex, t: target with $1..$9, s: status, x: excluded paths}.
|
|
27
|
+
const patternRules = /*PAPYRUS_PATTERN_RULES*/[]/*END_PAPYRUS_PATTERN_RULES*/;
|
|
28
|
+
|
|
29
|
+
function lookUpPatternRedirect(uri) {
|
|
30
|
+
for (let index = 0; index < patternRules.length; index += 1) {
|
|
31
|
+
const rule = patternRules[index];
|
|
32
|
+
if (rule.x.indexOf(uri) !== -1) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const match = new RegExp(rule.p).exec(uri);
|
|
36
|
+
if (match !== null) {
|
|
37
|
+
return {
|
|
38
|
+
status: rule.s,
|
|
39
|
+
target: rule.t.replace(/\$([0-9])/g, function (whole, group) {
|
|
40
|
+
return match[Number(group)] || "";
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
25
48
|
async function handler(event) {
|
|
26
49
|
const request = event.request;
|
|
27
50
|
const redirectTarget = await lookUpRedirectTarget(request.uri);
|
|
@@ -32,6 +55,14 @@ async function handler(event) {
|
|
|
32
55
|
headers: { location: { value: redirectTarget } },
|
|
33
56
|
};
|
|
34
57
|
}
|
|
58
|
+
const patternRedirect = lookUpPatternRedirect(request.uri);
|
|
59
|
+
if (patternRedirect !== null) {
|
|
60
|
+
return {
|
|
61
|
+
statusCode: patternRedirect.status,
|
|
62
|
+
statusDescription: "Moved",
|
|
63
|
+
headers: { location: { value: patternRedirect.target } },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
35
66
|
request.uri = rewriteToIndexDocument(request.uri);
|
|
36
67
|
return request;
|
|
37
68
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
const START_MARKER = "/*PAPYRUS_PATTERN_RULES*/";
|
|
4
|
+
const END_MARKER = "/*END_PAPYRUS_PATTERN_RULES*/";
|
|
5
|
+
/**
|
|
6
|
+
* The pattern rules a site declares in projection.json: its `redirects.patterns`, plus, for
|
|
7
|
+
* `urls.style: "directory"`, `/x.html` to `/x/`. Mirrors `StandardSettings.edge_rules` in Python.
|
|
8
|
+
*/
|
|
9
|
+
export function edgeRulesFromSettings(settings) {
|
|
10
|
+
const rules = (settings.redirects?.patterns ?? []).map((rule) => ({
|
|
11
|
+
p: rule.pattern,
|
|
12
|
+
t: rule.to,
|
|
13
|
+
s: rule.status ?? 301,
|
|
14
|
+
x: rule.except ?? [],
|
|
15
|
+
}));
|
|
16
|
+
if (settings.urls?.style === "directory") {
|
|
17
|
+
rules.push({ p: "^/(.+)\\.html$", t: "/$1/", s: 301, x: ["/404.html"] });
|
|
18
|
+
}
|
|
19
|
+
return rules;
|
|
20
|
+
}
|
|
21
|
+
export function readEdgeRules(staticDir, baseDirectory = process.cwd()) {
|
|
22
|
+
if (staticDir === undefined)
|
|
23
|
+
return [];
|
|
24
|
+
const settingsPath = resolve(baseDirectory, staticDir, "projection.json");
|
|
25
|
+
if (!existsSync(settingsPath))
|
|
26
|
+
return [];
|
|
27
|
+
return edgeRulesFromSettings(JSON.parse(readFileSync(settingsPath, "utf8")));
|
|
28
|
+
}
|
|
29
|
+
/** The CloudFront Function source with the site's pattern rules spliced in; the source unchanged when there are none. */
|
|
30
|
+
export function compileRedirectFunctionSource(source, rules) {
|
|
31
|
+
const start = source.indexOf(START_MARKER);
|
|
32
|
+
const end = source.indexOf(END_MARKER);
|
|
33
|
+
if (start === -1 || end === -1 || end < start) {
|
|
34
|
+
throw new Error("redirect function source has no pattern-rule markers");
|
|
35
|
+
}
|
|
36
|
+
if (rules.length === 0)
|
|
37
|
+
return source;
|
|
38
|
+
const literal = JSON.stringify(rules);
|
|
39
|
+
return source.slice(0, start + START_MARKER.length) + literal + source.slice(end);
|
|
40
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
/** One pattern redirect as the edge function reads it: regex, target with $1..$9, status, excluded paths. */
|
|
5
|
+
export type EdgeRedirectRule = { p: string; t: string; s: number; x: string[] };
|
|
6
|
+
|
|
7
|
+
const START_MARKER = "/*PAPYRUS_PATTERN_RULES*/";
|
|
8
|
+
const END_MARKER = "/*END_PAPYRUS_PATTERN_RULES*/";
|
|
9
|
+
|
|
10
|
+
type ProjectionRedirects = { patterns?: { pattern: string; to: string; status?: number; except?: string[] }[] };
|
|
11
|
+
type ProjectionSettings = { urls?: { style?: string }; redirects?: ProjectionRedirects };
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The pattern rules a site declares in projection.json: its `redirects.patterns`, plus, for
|
|
15
|
+
* `urls.style: "directory"`, `/x.html` to `/x/`. Mirrors `StandardSettings.edge_rules` in Python.
|
|
16
|
+
*/
|
|
17
|
+
export function edgeRulesFromSettings(settings: ProjectionSettings): EdgeRedirectRule[] {
|
|
18
|
+
const rules = (settings.redirects?.patterns ?? []).map((rule) => ({
|
|
19
|
+
p: rule.pattern,
|
|
20
|
+
t: rule.to,
|
|
21
|
+
s: rule.status ?? 301,
|
|
22
|
+
x: rule.except ?? [],
|
|
23
|
+
}));
|
|
24
|
+
if (settings.urls?.style === "directory") {
|
|
25
|
+
rules.push({ p: "^/(.+)\\.html$", t: "/$1/", s: 301, x: ["/404.html"] });
|
|
26
|
+
}
|
|
27
|
+
return rules;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function readEdgeRules(staticDir: string | undefined, baseDirectory: string = process.cwd()): EdgeRedirectRule[] {
|
|
31
|
+
if (staticDir === undefined) return [];
|
|
32
|
+
const settingsPath = resolve(baseDirectory, staticDir, "projection.json");
|
|
33
|
+
if (!existsSync(settingsPath)) return [];
|
|
34
|
+
return edgeRulesFromSettings(JSON.parse(readFileSync(settingsPath, "utf8")) as ProjectionSettings);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The CloudFront Function source with the site's pattern rules spliced in; the source unchanged when there are none. */
|
|
38
|
+
export function compileRedirectFunctionSource(source: string, rules: EdgeRedirectRule[]): string {
|
|
39
|
+
const start = source.indexOf(START_MARKER);
|
|
40
|
+
const end = source.indexOf(END_MARKER);
|
|
41
|
+
if (start === -1 || end === -1 || end < start) {
|
|
42
|
+
throw new Error("redirect function source has no pattern-rule markers");
|
|
43
|
+
}
|
|
44
|
+
if (rules.length === 0) return source;
|
|
45
|
+
const literal = JSON.stringify(rules);
|
|
46
|
+
return source.slice(0, start + START_MARKER.length) + literal + source.slice(end);
|
|
47
|
+
}
|
|
@@ -7,11 +7,13 @@ import { SqsDestination } from "aws-cdk-lib/aws-lambda-destinations";
|
|
|
7
7
|
import * as route53 from "aws-cdk-lib/aws-route53";
|
|
8
8
|
import * as s3 from "aws-cdk-lib/aws-s3";
|
|
9
9
|
import * as sqs from "aws-cdk-lib/aws-sqs";
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
10
11
|
import { dirname, resolve } from "node:path";
|
|
11
12
|
import { fileURLToPath } from "node:url";
|
|
12
13
|
import { assertValidPublicationConfig } from "../site-backend-config.js";
|
|
13
14
|
import { addRenditionsFunction } from "./renditions-function.js";
|
|
14
15
|
import { RENDITIONS_FUNCTION_NAME_ENV } from "./renditions-settings.js";
|
|
16
|
+
import { compileRedirectFunctionSource, readEdgeRules } from "./redirect-rules.js";
|
|
15
17
|
import { addInvalidationPathAlarm } from "./invalidation-alarm.js";
|
|
16
18
|
import { distributionIdFromArn, s3ProjectionEnvironment, s3ProjectionPolicyStatements } from "./s3-projection-settings.js";
|
|
17
19
|
const redirectFunctionSourcePath = resolve(dirname(fileURLToPath(import.meta.url)), "redirect-and-index.js");
|
|
@@ -70,7 +72,7 @@ export function addS3ProjectionPublication(scope, props, media) {
|
|
|
70
72
|
const keyValueStore = new cloudfront.KeyValueStore(scope, "RedirectsKeyValueStore");
|
|
71
73
|
const redirectFunction = new cloudfront.Function(scope, "RedirectAndIndexFunction", {
|
|
72
74
|
runtime: cloudfront.FunctionRuntime.JS_2_0,
|
|
73
|
-
code: cloudfront.FunctionCode.
|
|
75
|
+
code: cloudfront.FunctionCode.fromInline(compileRedirectFunctionSource(readFileSync(redirectFunctionSourcePath, "utf8"), readEdgeRules(props.staticDir))),
|
|
74
76
|
keyValueStore,
|
|
75
77
|
});
|
|
76
78
|
const deadLetterQueue = new sqs.Queue(scope, "ProjectionDeadLetterQueue", {
|
|
@@ -10,11 +10,13 @@ import * as route53 from "aws-cdk-lib/aws-route53";
|
|
|
10
10
|
import * as s3 from "aws-cdk-lib/aws-s3";
|
|
11
11
|
import * as sqs from "aws-cdk-lib/aws-sqs";
|
|
12
12
|
import type { Construct } from "constructs";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
13
14
|
import { dirname, resolve } from "node:path";
|
|
14
15
|
import { fileURLToPath } from "node:url";
|
|
15
16
|
import { assertValidPublicationConfig, type PapyrusPublicationConfig } from "../site-backend-config";
|
|
16
17
|
import { addRenditionsFunction, type RenditionsFunction } from "./renditions-function";
|
|
17
18
|
import { RENDITIONS_FUNCTION_NAME_ENV, type RenditionsPlan } from "./renditions-settings";
|
|
19
|
+
import { compileRedirectFunctionSource, readEdgeRules } from "./redirect-rules";
|
|
18
20
|
import { addInvalidationPathAlarm } from "./invalidation-alarm";
|
|
19
21
|
import { distributionIdFromArn, s3ProjectionEnvironment, s3ProjectionPolicyStatements } from "./s3-projection-settings";
|
|
20
22
|
|
|
@@ -112,7 +114,9 @@ export function addS3ProjectionPublication(
|
|
|
112
114
|
const keyValueStore = new cloudfront.KeyValueStore(scope, "RedirectsKeyValueStore");
|
|
113
115
|
const redirectFunction = new cloudfront.Function(scope, "RedirectAndIndexFunction", {
|
|
114
116
|
runtime: cloudfront.FunctionRuntime.JS_2_0,
|
|
115
|
-
code: cloudfront.FunctionCode.
|
|
117
|
+
code: cloudfront.FunctionCode.fromInline(
|
|
118
|
+
compileRedirectFunctionSource(readFileSync(redirectFunctionSourcePath, "utf8"), readEdgeRules(props.staticDir)),
|
|
119
|
+
),
|
|
116
120
|
keyValueStore,
|
|
117
121
|
});
|
|
118
122
|
const deadLetterQueue = new sqs.Queue(scope, "ProjectionDeadLetterQueue", {
|
|
@@ -67,8 +67,8 @@ function lines(text: string): string[] {
|
|
|
67
67
|
return text.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/** The policy as
|
|
71
|
-
function
|
|
70
|
+
/** The policy as Humanus sees it (its key is the policy id). Throws on invalid detector JSON. */
|
|
71
|
+
function humanusPolicy(draft: Draft): Record<string, unknown> {
|
|
72
72
|
const detectors = JSON.parse(draft.detectorsText || "[]") as unknown;
|
|
73
73
|
if (!Array.isArray(detectors)) throw new Error("Detectors must be a JSON list.");
|
|
74
74
|
const violates = lines(draft.violatesText);
|
|
@@ -133,7 +133,7 @@ export function EditorialChecksAdminPanel() {
|
|
|
133
133
|
const key = draft.record.policyKey.trim();
|
|
134
134
|
if (!POLICY_KEY.test(key)) throw new Error("The key must be lowercase letters, digits, dots, hyphens or underscores.");
|
|
135
135
|
if (!draft.record.title.trim() || !draft.record.guidance.trim()) throw new Error("A policy needs a title and guidance.");
|
|
136
|
-
const policy =
|
|
136
|
+
const policy = humanusPolicy(draft);
|
|
137
137
|
const record: EditorialPolicyRecord = {
|
|
138
138
|
...draft.record,
|
|
139
139
|
id: draft.isNew ? `${draft.record.scope}#${key}` : draft.record.id,
|
|
@@ -252,7 +252,7 @@ export function EditorialChecksAdminPanel() {
|
|
|
252
252
|
<label>Examples that break it (one per line) <Textarea onChange={(event) => setDraft({ ...draft, violatesText: event.target.value })} rows={3} value={draft.violatesText} /></label>
|
|
253
253
|
<label>Examples that follow it (one per line) <Textarea onChange={(event) => setDraft({ ...draft, compliesText: event.target.value })} rows={3} value={draft.compliesText} /></label>
|
|
254
254
|
<label>
|
|
255
|
-
Detectors (JSON; see <code>
|
|
255
|
+
Detectors (JSON; see <code>humanus schema policy-pack-v1</code>)
|
|
256
256
|
<Textarea className="font-mono text-xs" onChange={(event) => setDraft({ ...draft, detectorsText: event.target.value })} rows={10} value={draft.detectorsText} />
|
|
257
257
|
</label>
|
|
258
258
|
<div className="flex flex-wrap gap-2">
|
|
@@ -293,7 +293,7 @@ export function EditorialChecksAdminPanel() {
|
|
|
293
293
|
<Button
|
|
294
294
|
disabled={working !== null || !tryText.trim()}
|
|
295
295
|
onClick={() => run("try", async () => {
|
|
296
|
-
setTryResult(await tryPolicy(
|
|
296
|
+
setTryResult(await tryPolicy(humanusPolicy(draft), tryText));
|
|
297
297
|
})}
|
|
298
298
|
size="sm"
|
|
299
299
|
type="button"
|
|
@@ -200,7 +200,6 @@ export type ProcedureRunRecord = {
|
|
|
200
200
|
newsroomFeedKey?: string | null;
|
|
201
201
|
};
|
|
202
202
|
|
|
203
|
-
const DEFAULT_NEWSROOM_SECTIONS_CONFIG_PATH = path.join(process.cwd(), "corpora", "papyrus-newsroom-sections.yml");
|
|
204
203
|
const NEWSROOM_SECTION_TYPES = new Set<NewsroomSectionType>(["canonical", "floating", "rotating"]);
|
|
205
204
|
type NewsroomSectionSeedRow = Omit<NewsroomSectionRecord, "sortOrder" | "enabled" | "enabledStatus" | "createdAt" | "updatedAt"> & {
|
|
206
205
|
enabled: boolean;
|
|
@@ -690,10 +689,10 @@ export type CategorySteeringDashboard = {
|
|
|
690
689
|
loadError?: string | null;
|
|
691
690
|
};
|
|
692
691
|
|
|
693
|
-
const DEFAULT_ANALYSIS_PROFILES_PATH = path.join(process.cwd(), "corpora", "papyrus-analysis-profiles.yml");
|
|
694
|
-
const DEFAULT_STEERING_CONFIG_PATH = path.join(process.cwd(), "corpora", "papyrus-steering.yml");
|
|
695
692
|
|
|
696
|
-
|
|
693
|
+
/** Analysis profiles the site keeps in a file; a site that configures none (data-driven sites) has none. No default file is looked up. */
|
|
694
|
+
export async function loadAnalysisProfileSummaries(filepath?: string): Promise<AnalysisProfileSummary[]> {
|
|
695
|
+
if (!filepath) return [];
|
|
697
696
|
try {
|
|
698
697
|
const parsed = YAML.parse(fs.readFileSync(filepath, "utf8")) as {
|
|
699
698
|
schemaVersion?: number;
|
|
@@ -722,7 +721,9 @@ export async function loadAnalysisProfileSummaries(filepath = DEFAULT_ANALYSIS_P
|
|
|
722
721
|
}
|
|
723
722
|
}
|
|
724
723
|
|
|
725
|
-
|
|
724
|
+
/** Corpora a site lists in a steering file; no configured file means none. No default file is looked up. */
|
|
725
|
+
export async function loadConfiguredCorpusSummaries(filepath?: string): Promise<CategorySteeringCorpus[]> {
|
|
726
|
+
if (!filepath) return [];
|
|
726
727
|
try {
|
|
727
728
|
const parsed = YAML.parse(fs.readFileSync(filepath, "utf8")) as {
|
|
728
729
|
corpora?: Array<Record<string, unknown>>;
|
|
@@ -818,7 +819,8 @@ function sortCategorys(categorys: CategorySteeringCategory[]): CategorySteeringC
|
|
|
818
819
|
});
|
|
819
820
|
}
|
|
820
821
|
|
|
821
|
-
function defaultNewsroomSections(importedAt: string, sectionsConfigPath
|
|
822
|
+
function defaultNewsroomSections(importedAt: string, sectionsConfigPath?: string): NewsroomSectionRecord[] {
|
|
823
|
+
if (!sectionsConfigPath) return [];
|
|
822
824
|
return loadNewsroomSectionSeedRows(sectionsConfigPath).map((row, index) => ({
|
|
823
825
|
...row,
|
|
824
826
|
enabledStatus: "enabled",
|
|
@@ -828,7 +830,7 @@ function defaultNewsroomSections(importedAt: string, sectionsConfigPath = DEFAUL
|
|
|
828
830
|
}));
|
|
829
831
|
}
|
|
830
832
|
|
|
831
|
-
function loadNewsroomSectionSeedRows(sectionsConfigPath
|
|
833
|
+
function loadNewsroomSectionSeedRows(sectionsConfigPath: string) {
|
|
832
834
|
const cached = newsroomSectionSeedRowsCache.get(sectionsConfigPath);
|
|
833
835
|
if (cached) return cached;
|
|
834
836
|
const parsed = YAML.parse(fs.readFileSync(sectionsConfigPath, "utf8")) as {
|
|
@@ -883,7 +885,7 @@ function normalizeNewsroomSectionSeedRow(
|
|
|
883
885
|
|
|
884
886
|
export function createDemoCategorySteeringDashboard(
|
|
885
887
|
brandId: SiteBrandId = resolveSiteBrandId(),
|
|
886
|
-
newsroomSectionsConfigPath
|
|
888
|
+
newsroomSectionsConfigPath?: string,
|
|
887
889
|
): CategorySteeringDashboard {
|
|
888
890
|
const importedAt = "2026-05-16T12:00:00.000Z";
|
|
889
891
|
const profile = getNewsroomDemoProfile(brandId);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Demo-mode editorial checks: no backend, a few stock phrases, canned rewrites.
|
|
3
|
-
* The shapes match
|
|
3
|
+
* The shapes match Humanus results so the editor, the preview marks and the
|
|
4
4
|
* rewrite flow behave as they do against the real backend.
|
|
5
5
|
*/
|
|
6
6
|
import type { EditorialCheckResult, EditorialFinding, EditorialSegment, RewriteResult, Span } from "./editorial-checks";
|
package/lib/editorial-checks.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Editorial checks: the
|
|
3
|
-
* over them.
|
|
2
|
+
* Editorial checks: the Humanus result shapes the newsroom uses, and pure helpers
|
|
3
|
+
* over them. Humanus counts offsets in UTF-16 code units, which is exactly how
|
|
4
4
|
* JavaScript indexes strings, so spans apply to the body text as they are.
|
|
5
5
|
*/
|
|
6
6
|
|
package/lib/editorial-preview.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Places
|
|
2
|
+
* Places Humanus findings on the rendered Markus preview.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Humanus reports spans in the markup; the preview renders an IR tree with no
|
|
5
5
|
* character offsets. Both sides agree on the words a reader sees, so findings
|
|
6
|
-
* are placed by text: each
|
|
6
|
+
* are placed by text: each Humanus paragraph is matched, in document order, to
|
|
7
7
|
* the rendered prose block with the same words, and each finding's plain
|
|
8
8
|
* excerpt is located inside that block. Whitespace is ignored while matching,
|
|
9
|
-
* because rendering and
|
|
9
|
+
* because rendering and Humanus collapse it differently (citations, breaks).
|
|
10
10
|
*
|
|
11
11
|
* "Rendered text" is defined by `inlineLeaves`, which the preview renderer also
|
|
12
12
|
* uses, so offsets computed here are offsets the renderer can split on.
|
|
@@ -131,7 +131,7 @@ function blockKey(text: string): string {
|
|
|
131
131
|
return compact(text).value.replace(/[*_`~]/g, "");
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
/** Match every
|
|
134
|
+
/** Match every Humanus paragraph to a rendered block and place its findings and sentences there. */
|
|
135
135
|
export function planPreview(
|
|
136
136
|
document: MarkusDocument,
|
|
137
137
|
result: EditorialCheckResult,
|
package/lib/site-brand-tenant.ts
CHANGED
|
@@ -9,14 +9,19 @@ export function resolveBrandConfigPath(relativePath: string): string {
|
|
|
9
9
|
return path.isAbsolute(relativePath) ? relativePath : path.join(process.cwd(), relativePath);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
/** Optional repo files a brand may still name; undefined means the site keeps that configuration as backend data and nothing is looked up. */
|
|
13
|
+
function optionalBrandConfigPath(relativePath: string | undefined): string | undefined {
|
|
14
|
+
return relativePath ? resolveBrandConfigPath(relativePath) : undefined;
|
|
14
15
|
}
|
|
15
16
|
|
|
16
|
-
export function
|
|
17
|
-
return
|
|
17
|
+
export function getBrandSteeringConfigPath(brand: SiteBrand = resolveActiveSiteBrand()): string | undefined {
|
|
18
|
+
return optionalBrandConfigPath(brand.steeringConfigPath);
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
export function
|
|
21
|
-
return
|
|
21
|
+
export function getBrandNewsroomSectionsConfigPath(brand: SiteBrand = resolveActiveSiteBrand()): string | undefined {
|
|
22
|
+
return optionalBrandConfigPath(brand.newsroomSectionsConfigPath);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getBrandAnalysisProfilesPath(brand: SiteBrand = resolveActiveSiteBrand()): string | undefined {
|
|
26
|
+
return optionalBrandConfigPath(brand.analysisProfilesPath);
|
|
22
27
|
}
|
package/lib/site-brand.ts
CHANGED
|
@@ -142,9 +142,12 @@ export type SiteBrand = {
|
|
|
142
142
|
readerBasePath?: string;
|
|
143
143
|
opsChrome: OpsChrome;
|
|
144
144
|
corpusKey: string;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
/** Optional repo file for topic steering. Editorial and steering configuration is backend data; a data-driven site sets none and nothing is looked up. */
|
|
146
|
+
steeringConfigPath?: string;
|
|
147
|
+
/** Optional repo file for the local-preview demo sections only. Sections live in the backend. */
|
|
148
|
+
newsroomSectionsConfigPath?: string;
|
|
149
|
+
/** Optional repo file listing analysis profiles. */
|
|
150
|
+
analysisProfilesPath?: string;
|
|
148
151
|
publicationName: string;
|
|
149
152
|
components?: SiteBrandComponents;
|
|
150
153
|
/**
|
package/package.json
CHANGED
package/routes.manifest.json
CHANGED