@cosla/sensemaking-report-builder 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -55,7 +55,9 @@ Defaults: opinions `<inputDir>/opinions.csv`, summary `<inputDir>/summary.json`,
55
55
 
56
56
  | Arg / flag | Purpose | Default |
57
57
  |------------|---------|---------|
58
- | `inline` \| `static` | Build mode (**required**) | — |
58
+ | `inline` \| `static` | Build mode (**required** for builds) | — |
59
+ | `config-schema` | Print config JSON Schema to stdout (no inputs) | — |
60
+ | `--schema-version` | With `config-schema`: schema version to print | latest (`1`) |
59
61
  | `--inputDir` | Base dir for default input paths | `./input` |
60
62
  | `--opinions` / `--bridging_scores` | Opinions / bridging scores CSV | `<inputDir>/opinions.csv` |
61
63
  | `--summary` | Summary JSON (`report_data.json`) | `<inputDir>/summary.json` |
@@ -64,6 +66,19 @@ Defaults: opinions `<inputDir>/opinions.csv`, summary `<inputDir>/summary.json`,
64
66
  | `--output` | **inline only:** path to HTML file | `./output/report.html` |
65
67
  | `--outputDir` | **static only:** output directory | `./output` |
66
68
 
69
+ ### Config schema
70
+
71
+ ```bash
72
+ npx @cosla/sensemaking-report-builder config-schema
73
+ npx @cosla/sensemaking-report-builder config-schema --schema-version 1
74
+ ```
75
+
76
+ Prints a [JSON Schema draft 2020-12](https://json-schema.org/draft/2020-12/schema) document for supported `config.json` keys (`schemas/config.v1.json`), including application field `config_schema_version`. Unknown `--schema-version` values fail with a clear error.
77
+
78
+ ```bash
79
+ npm run smoke # validates config-schema output
80
+ ```
81
+
67
82
  Logo and translations are configured in `config.json` (not CLI flags). Place `logo.svg` / `logo.png` next to that config (under `--inputDir` when using dir mode).
68
83
 
69
84
  ## Input contracts
package/bin/cli.js CHANGED
@@ -3,6 +3,7 @@ import { runBuild } from "../build.js";
3
3
 
4
4
  runBuild(process.argv, process.cwd())
5
5
  .then((result) => {
6
+ if (result.command === "config-schema") return;
6
7
  const out = result.output || result.outputDir;
7
8
  console.log(`Standalone report generated: ${out}`);
8
9
  })
package/build.js CHANGED
@@ -10,7 +10,11 @@ import { execSync } from "node:child_process";
10
10
  import csv from "csvtojson";
11
11
  import mustache from "mustache";
12
12
  import { inlineSource } from "inline-source";
13
- import { processReportData, resolveBuildOptions } from "./data.js";
13
+ import {
14
+ loadConfigSchema,
15
+ processReportData,
16
+ resolveBuildOptions,
17
+ } from "./data.js";
14
18
 
15
19
  const WORK_DIR_NAME = ".py-report-ui-work";
16
20
 
@@ -173,6 +177,13 @@ function inlineFontDataUris(html, fontsDir) {
173
177
  export async function runBuild(argv = process.argv, cwd = process.cwd()) {
174
178
  const options = resolveBuildOptions(argv, cwd);
175
179
  const packageRoot = path.dirname(fileURLToPath(import.meta.url));
180
+
181
+ if (options.command === "config-schema") {
182
+ const schema = loadConfigSchema(options.schemaVersion, packageRoot);
183
+ process.stdout.write(`${JSON.stringify(schema, null, 2)}\n`);
184
+ return { command: "config-schema" };
185
+ }
186
+
176
187
  const artefactRoot =
177
188
  options.command === "inline"
178
189
  ? path.dirname(options.output)
@@ -296,6 +307,7 @@ if (isMain) {
296
307
  } else {
297
308
  runBuild(process.argv, process.cwd())
298
309
  .then((result) => {
310
+ if (result.command === "config-schema") return;
299
311
  console.log(
300
312
  `Standalone report generated: ${result.output || result.outputDir}`,
301
313
  );
package/data.js CHANGED
@@ -24,18 +24,50 @@ const demographics_prefix = "demo:";
24
24
 
25
25
  /**
26
26
  * @typedef {Object} BuildOptions
27
- * @property {"inline"|"static"} command
28
- * @property {string} inputDir
29
- * @property {string|null} output
30
- * @property {string|null} outputDir
31
- * @property {string} opinionsPath
32
- * @property {string} summaryPath
33
- * @property {string|null} predictedPath
34
- * @property {string|null} configPath
35
- * @property {boolean} predictedExplicit
36
- * @property {boolean} configExplicit
27
+ * @property {"inline"|"static"|"config-schema"} command
28
+ * @property {string|null} [schemaVersion]
29
+ * @property {string} [inputDir]
30
+ * @property {string|null} [output]
31
+ * @property {string|null} [outputDir]
32
+ * @property {string} [opinionsPath]
33
+ * @property {string} [summaryPath]
34
+ * @property {string|null} [predictedPath]
35
+ * @property {string|null} [configPath]
36
+ * @property {boolean} [predictedExplicit]
37
+ * @property {boolean} [configExplicit]
37
38
  */
38
39
 
40
+ /** Latest shipped config schema version (omit `--schema-version` → this). */
41
+ export const LATEST_CONFIG_SCHEMA_VERSION = 1;
42
+
43
+ const CONFIG_SCHEMA_FILES = {
44
+ 1: "config.v1.json",
45
+ };
46
+
47
+ /**
48
+ * Loads a config JSON Schema document for the given version.
49
+ * @param {string|number|null|undefined} schemaVersion
50
+ * @param {string} packageRoot Absolute path to the package root (directory of build.js).
51
+ * @returns {object}
52
+ */
53
+ export function loadConfigSchema(schemaVersion, packageRoot) {
54
+ const version =
55
+ schemaVersion === undefined || schemaVersion === null || schemaVersion === ""
56
+ ? LATEST_CONFIG_SCHEMA_VERSION
57
+ : Number(schemaVersion);
58
+ if (!Number.isInteger(version) || !CONFIG_SCHEMA_FILES[version]) {
59
+ const supported = Object.keys(CONFIG_SCHEMA_FILES).join(", ");
60
+ throw new Error(
61
+ `Unknown config schema version "${schemaVersion}". Supported: ${supported}.`,
62
+ );
63
+ }
64
+ const schemaPath = path.join(packageRoot, "schemas", CONFIG_SCHEMA_FILES[version]);
65
+ if (!fs.existsSync(schemaPath)) {
66
+ throw new Error(`Config schema file not found: ${schemaPath}`);
67
+ }
68
+ return JSON.parse(fs.readFileSync(schemaPath, "utf-8"));
69
+ }
70
+
39
71
  /**
40
72
  * Parses argv flags into a Map of key -> value.
41
73
  * @param {string[]} args
@@ -69,16 +101,26 @@ export function resolveBuildOptions(argv = process.argv, cwd = process.cwd()) {
69
101
  const commandToken = args[0] && !args[0].startsWith("--") ? args[0] : null;
70
102
  if (!commandToken) {
71
103
  throw new Error(
72
- 'Build mode is required. Use "inline" or "static" as the first argument.',
104
+ 'Command is required. Use "inline", "static", or "config-schema" as the first argument.',
73
105
  );
74
106
  }
107
+
108
+ const flags = parseFlags(args);
109
+
110
+ if (commandToken === "config-schema") {
111
+ return {
112
+ command: commandToken,
113
+ schemaVersion: flags.has("schema-version")
114
+ ? flags.get("schema-version")
115
+ : null,
116
+ };
117
+ }
118
+
75
119
  if (commandToken !== "inline" && commandToken !== "static") {
76
120
  throw new Error(
77
- `Unsupported command "${commandToken}". Use "inline" or "static".`,
121
+ `Unsupported command "${commandToken}". Use "inline", "static", or "config-schema".`,
78
122
  );
79
123
  }
80
-
81
- const flags = parseFlags(args);
82
124
  const hasOutput = flags.has("output");
83
125
  const hasOutputDir = flags.has("outputDir");
84
126
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosla/sensemaking-report-builder",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Interactive HTML report builder for Cosla Python sensemaking pipeline outputs.",
5
5
  "type": "module",
6
6
  "main": "build.js",
@@ -12,7 +12,9 @@
12
12
  "build.js",
13
13
  "data.js",
14
14
  "src/**/*",
15
- "README.md"
15
+ "schemas/**/*",
16
+ "README.md",
17
+ "smoke.js"
16
18
  ],
17
19
  "scripts": {
18
20
  "predev": "node bin/cli.js static --outputDir ./output",
@@ -21,7 +23,9 @@
21
23
  "inline": "node bin/cli.js inline",
22
24
  "build": "node bin/cli.js inline",
23
25
  "preview": "node build.js preview",
24
- "github": "node build.js github"
26
+ "github": "node build.js github",
27
+ "smoke": "node smoke.js",
28
+ "test": "npm run smoke"
25
29
  },
26
30
  "repository": {
27
31
  "type": "git",
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "config_schema_version": 1,
4
+ "title": "PythonReportConfig",
5
+ "type": "object",
6
+ "additionalProperties": true,
7
+ "properties": {
8
+ "title": {
9
+ "type": "string",
10
+ "title": "Report Title Override"
11
+ },
12
+ "logo": {
13
+ "type": "string",
14
+ "title": "Logo Image Filename"
15
+ },
16
+ "excluded_topics": {
17
+ "type": "array",
18
+ "title": "Excluded Topics",
19
+ "description": "Topic names to omit (exact match)",
20
+ "items": {
21
+ "type": "string"
22
+ }
23
+ },
24
+ "excluded_opinions": {
25
+ "type": "array",
26
+ "title": "Excluded Opinions",
27
+ "description": "Opinion names to omit (exact match)",
28
+ "items": {
29
+ "type": "string"
30
+ }
31
+ }
32
+ }
33
+ }
package/smoke.js ADDED
@@ -0,0 +1,62 @@
1
+ import { loadConfigSchema, LATEST_CONFIG_SCHEMA_VERSION } from "./data.js";
2
+ import { runBuild } from "./build.js";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+
6
+ const packageRoot = path.dirname(fileURLToPath(import.meta.url));
7
+
8
+ function assert(condition, message) {
9
+ if (!condition) {
10
+ throw new Error(message);
11
+ }
12
+ }
13
+
14
+ async function main() {
15
+ const schema = loadConfigSchema(null, packageRoot);
16
+ assert(
17
+ schema.config_schema_version === LATEST_CONFIG_SCHEMA_VERSION,
18
+ `Expected config_schema_version ${LATEST_CONFIG_SCHEMA_VERSION}`,
19
+ );
20
+ for (const key of ["title", "logo", "excluded_topics", "excluded_opinions"]) {
21
+ assert(schema.properties?.[key], `Config schema missing property "${key}"`);
22
+ }
23
+
24
+ const explicit = loadConfigSchema(1, packageRoot);
25
+ assert(explicit.config_schema_version === 1, "Expected schema version 1");
26
+
27
+ let failed = false;
28
+ try {
29
+ loadConfigSchema(99, packageRoot);
30
+ } catch (error) {
31
+ failed = true;
32
+ assert(
33
+ String(error.message).includes("Unknown config schema version"),
34
+ `Expected clear unknown-version error, got: ${error.message}`,
35
+ );
36
+ }
37
+ assert(failed, "Expected unknown schema version to fail.");
38
+
39
+ const previousWrite = process.stdout.write.bind(process.stdout);
40
+ let captured = "";
41
+ process.stdout.write = (chunk) => {
42
+ captured += String(chunk);
43
+ return true;
44
+ };
45
+ try {
46
+ await runBuild(["node", "build.js", "config-schema"], packageRoot);
47
+ } finally {
48
+ process.stdout.write = previousWrite;
49
+ }
50
+ const printed = JSON.parse(captured);
51
+ assert(
52
+ printed.config_schema_version === 1,
53
+ "config-schema command did not print v1 schema",
54
+ );
55
+
56
+ console.log("Smoke tests passed.");
57
+ }
58
+
59
+ main().catch((error) => {
60
+ console.error(error.message);
61
+ process.exit(1);
62
+ });