@praxisflux/gates 0.59.0 → 0.59.3

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.
@@ -36,6 +36,25 @@
36
36
  //
37
37
  // Dual-use: `node lib/board-mirror.mjs --check --root <dir>` mechanizes drift for a
38
38
  // deterministic provider and staleness for a model-backed one (see the CLI block below).
39
+ //
40
+ // `.board.json` (spec 054) is the OTHER file this module owns, and a different question from
41
+ // the mirror above: `.board/links.json` is BOARD STATE (what's linked, its status);
42
+ // `.board.json` is BOARD CONFIG (which provider a project uses, and that provider's
43
+ // coordinates). Tracked, hand-editable, at the project root, outside every marker — same
44
+ // posture as `.claude/model-tiers.json` (plant only when absent; doctrine points at the config;
45
+ // the config is what you edit, not something generated). `loadBoardConfig`/`validateBoardConfig`
46
+ // near the `providers` registry below read/check it.
47
+ //
48
+ // `.board.json`'s `statusMap` (bridge status -> site workflow status) composes with
49
+ // `.spec-bridge.json`'s `statusVocabulary` (derivation stage -> bridge status, bridge.mjs:69)
50
+ // at a DIFFERENT layer — the two do not merge, and this is the written precedence:
51
+ //
52
+ // derivation stage ──statusVocabulary──▶ bridge status ──statusMap──▶ site workflow status
53
+ // (reviewing) ("In Review") ("In Review")
54
+ //
55
+ // Unmapped statuses fall through unchanged at either layer. Neither file's meaning changes;
56
+ // this is only the stated relationship between them (two undocumented status mappings is a bug
57
+ // factory).
39
58
 
40
59
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
41
60
  import { join, dirname, resolve } from "node:path";
@@ -256,6 +275,68 @@ export const providers = {
256
275
  backlog: { requiresSync: false, project: projectBacklog },
257
276
  };
258
277
 
278
+ /* ── `.board.json` config schema (spec 054) — a SEPARATE, smaller table from `providers`
279
+ * above. `providers` is the projector registry (spec 052/056): provider name -> how to
280
+ * recompute the mirror. This table only knows the config-schema shape — which provider names
281
+ * are legal and which fields each requires — so `.board.json` can be validated today without
282
+ * pre-empting spec 056's ownership of adding `jira`'s projector entry to `providers`. Do NOT
283
+ * fold these two tables into one: that would re-couple config validation to projector
284
+ * implementation, which is exactly the coupling keeping them apart avoids. */
285
+ const BOARD_CONFIG_PROVIDERS = {
286
+ backlog: { requiredFields: [] },
287
+ jira: { requiredFields: ["cloudId", "projectKey", "issueTypeName"] },
288
+ };
289
+
290
+ /** Load `<root>/.board.json`. Returns `{ provider: "backlog" }` when absent — spec 053's
291
+ * backward-compatible default. Throws on malformed JSON (fail closed). Throws naming the known
292
+ * providers on an unknown `provider` value — NEVER falls back to `backlog`: silently treating
293
+ * a Jira project as a Backlog project is the exact silent no-op this feature exists to
294
+ * remove. */
295
+ export function loadBoardConfig(root) {
296
+ const path = join(root, ".board.json");
297
+ if (!existsSync(path)) return { provider: "backlog" };
298
+ const raw = readFileSync(path, "utf8");
299
+ let config;
300
+ try {
301
+ config = JSON.parse(raw);
302
+ } catch (e) {
303
+ throw new Error(`${path}: malformed JSON (${e.message})`);
304
+ }
305
+ const name = config?.provider;
306
+ if (!BOARD_CONFIG_PROVIDERS[name])
307
+ throw new Error(`${path}: unknown board provider ${JSON.stringify(name)} (known: ${Object.keys(BOARD_CONFIG_PROVIDERS).join(", ")})`);
308
+ return config;
309
+ }
310
+
311
+ /** Validate a `.board.json` object (as returned by `loadBoardConfig` or built in memory).
312
+ * Returns human-readable problems, empty when valid. Catches: `provider` as an array (one
313
+ * board is the plan of record — a list is a validation error saying why) or other non-string,
314
+ * an unknown provider name, a known provider missing one of its required fields (`jira` needs
315
+ * `cloudId`/`projectKey`/`issueTypeName`), and a non-object `statusMap`. */
316
+ export function validateBoardConfig(config) {
317
+ const problems = [];
318
+ if (!config || typeof config !== "object") {
319
+ problems.push("config: expected object");
320
+ return problems;
321
+ }
322
+ const name = config.provider;
323
+ if (Array.isArray(name)) {
324
+ problems.push("provider: expected a single string, got an array (one board is the plan of record)");
325
+ } else if (typeof name !== "string") {
326
+ problems.push(`provider: expected string, got ${name === undefined ? "missing" : typeof name}`);
327
+ } else if (!BOARD_CONFIG_PROVIDERS[name]) {
328
+ problems.push(`provider: unknown board provider "${name}" (known: ${Object.keys(BOARD_CONFIG_PROVIDERS).join(", ")})`);
329
+ } else {
330
+ const { requiredFields } = BOARD_CONFIG_PROVIDERS[name];
331
+ const sub = config[name] || {};
332
+ for (const field of requiredFields)
333
+ if (!sub[field]) problems.push(`${name}.${field}: required for provider "${name}"`);
334
+ if (sub.statusMap !== undefined && (typeof sub.statusMap !== "object" || Array.isArray(sub.statusMap) || sub.statusMap === null))
335
+ problems.push(`${name}.statusMap: expected object, got ${Array.isArray(sub.statusMap) ? "array" : typeof sub.statusMap}`);
336
+ }
337
+ return problems;
338
+ }
339
+
259
340
  /** Run git argv `args` in `cwd`. Never throws — a git failure is data, not an exception,
260
341
  * matching grounding-wiki/gates/repin-window.mjs's `git()` helper shape (spawnSync, argv
261
342
  * array so there is no shell, utf8 encoding). Returns `{ status, out }`; `status` is `null`
@@ -36,6 +36,25 @@
36
36
  //
37
37
  // Dual-use: `node lib/board-mirror.mjs --check --root <dir>` mechanizes drift for a
38
38
  // deterministic provider and staleness for a model-backed one (see the CLI block below).
39
+ //
40
+ // `.board.json` (spec 054) is the OTHER file this module owns, and a different question from
41
+ // the mirror above: `.board/links.json` is BOARD STATE (what's linked, its status);
42
+ // `.board.json` is BOARD CONFIG (which provider a project uses, and that provider's
43
+ // coordinates). Tracked, hand-editable, at the project root, outside every marker — same
44
+ // posture as `.claude/model-tiers.json` (plant only when absent; doctrine points at the config;
45
+ // the config is what you edit, not something generated). `loadBoardConfig`/`validateBoardConfig`
46
+ // near the `providers` registry below read/check it.
47
+ //
48
+ // `.board.json`'s `statusMap` (bridge status -> site workflow status) composes with
49
+ // `.spec-bridge.json`'s `statusVocabulary` (derivation stage -> bridge status, bridge.mjs:69)
50
+ // at a DIFFERENT layer — the two do not merge, and this is the written precedence:
51
+ //
52
+ // derivation stage ──statusVocabulary──▶ bridge status ──statusMap──▶ site workflow status
53
+ // (reviewing) ("In Review") ("In Review")
54
+ //
55
+ // Unmapped statuses fall through unchanged at either layer. Neither file's meaning changes;
56
+ // this is only the stated relationship between them (two undocumented status mappings is a bug
57
+ // factory).
39
58
 
40
59
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
41
60
  import { join, dirname, resolve } from "node:path";
@@ -256,6 +275,68 @@ export const providers = {
256
275
  backlog: { requiresSync: false, project: projectBacklog },
257
276
  };
258
277
 
278
+ /* ── `.board.json` config schema (spec 054) — a SEPARATE, smaller table from `providers`
279
+ * above. `providers` is the projector registry (spec 052/056): provider name -> how to
280
+ * recompute the mirror. This table only knows the config-schema shape — which provider names
281
+ * are legal and which fields each requires — so `.board.json` can be validated today without
282
+ * pre-empting spec 056's ownership of adding `jira`'s projector entry to `providers`. Do NOT
283
+ * fold these two tables into one: that would re-couple config validation to projector
284
+ * implementation, which is exactly the coupling keeping them apart avoids. */
285
+ const BOARD_CONFIG_PROVIDERS = {
286
+ backlog: { requiredFields: [] },
287
+ jira: { requiredFields: ["cloudId", "projectKey", "issueTypeName"] },
288
+ };
289
+
290
+ /** Load `<root>/.board.json`. Returns `{ provider: "backlog" }` when absent — spec 053's
291
+ * backward-compatible default. Throws on malformed JSON (fail closed). Throws naming the known
292
+ * providers on an unknown `provider` value — NEVER falls back to `backlog`: silently treating
293
+ * a Jira project as a Backlog project is the exact silent no-op this feature exists to
294
+ * remove. */
295
+ export function loadBoardConfig(root) {
296
+ const path = join(root, ".board.json");
297
+ if (!existsSync(path)) return { provider: "backlog" };
298
+ const raw = readFileSync(path, "utf8");
299
+ let config;
300
+ try {
301
+ config = JSON.parse(raw);
302
+ } catch (e) {
303
+ throw new Error(`${path}: malformed JSON (${e.message})`);
304
+ }
305
+ const name = config?.provider;
306
+ if (!BOARD_CONFIG_PROVIDERS[name])
307
+ throw new Error(`${path}: unknown board provider ${JSON.stringify(name)} (known: ${Object.keys(BOARD_CONFIG_PROVIDERS).join(", ")})`);
308
+ return config;
309
+ }
310
+
311
+ /** Validate a `.board.json` object (as returned by `loadBoardConfig` or built in memory).
312
+ * Returns human-readable problems, empty when valid. Catches: `provider` as an array (one
313
+ * board is the plan of record — a list is a validation error saying why) or other non-string,
314
+ * an unknown provider name, a known provider missing one of its required fields (`jira` needs
315
+ * `cloudId`/`projectKey`/`issueTypeName`), and a non-object `statusMap`. */
316
+ export function validateBoardConfig(config) {
317
+ const problems = [];
318
+ if (!config || typeof config !== "object") {
319
+ problems.push("config: expected object");
320
+ return problems;
321
+ }
322
+ const name = config.provider;
323
+ if (Array.isArray(name)) {
324
+ problems.push("provider: expected a single string, got an array (one board is the plan of record)");
325
+ } else if (typeof name !== "string") {
326
+ problems.push(`provider: expected string, got ${name === undefined ? "missing" : typeof name}`);
327
+ } else if (!BOARD_CONFIG_PROVIDERS[name]) {
328
+ problems.push(`provider: unknown board provider "${name}" (known: ${Object.keys(BOARD_CONFIG_PROVIDERS).join(", ")})`);
329
+ } else {
330
+ const { requiredFields } = BOARD_CONFIG_PROVIDERS[name];
331
+ const sub = config[name] || {};
332
+ for (const field of requiredFields)
333
+ if (!sub[field]) problems.push(`${name}.${field}: required for provider "${name}"`);
334
+ if (sub.statusMap !== undefined && (typeof sub.statusMap !== "object" || Array.isArray(sub.statusMap) || sub.statusMap === null))
335
+ problems.push(`${name}.statusMap: expected object, got ${Array.isArray(sub.statusMap) ? "array" : typeof sub.statusMap}`);
336
+ }
337
+ return problems;
338
+ }
339
+
259
340
  /** Run git argv `args` in `cwd`. Never throws — a git failure is data, not an exception,
260
341
  * matching grounding-wiki/gates/repin-window.mjs's `git()` helper shape (spawnSync, argv
261
342
  * array so there is no shell, utf8 encoding). Returns `{ status, out }`; `status` is `null`
@@ -36,6 +36,25 @@
36
36
  //
37
37
  // Dual-use: `node lib/board-mirror.mjs --check --root <dir>` mechanizes drift for a
38
38
  // deterministic provider and staleness for a model-backed one (see the CLI block below).
39
+ //
40
+ // `.board.json` (spec 054) is the OTHER file this module owns, and a different question from
41
+ // the mirror above: `.board/links.json` is BOARD STATE (what's linked, its status);
42
+ // `.board.json` is BOARD CONFIG (which provider a project uses, and that provider's
43
+ // coordinates). Tracked, hand-editable, at the project root, outside every marker — same
44
+ // posture as `.claude/model-tiers.json` (plant only when absent; doctrine points at the config;
45
+ // the config is what you edit, not something generated). `loadBoardConfig`/`validateBoardConfig`
46
+ // near the `providers` registry below read/check it.
47
+ //
48
+ // `.board.json`'s `statusMap` (bridge status -> site workflow status) composes with
49
+ // `.spec-bridge.json`'s `statusVocabulary` (derivation stage -> bridge status, bridge.mjs:69)
50
+ // at a DIFFERENT layer — the two do not merge, and this is the written precedence:
51
+ //
52
+ // derivation stage ──statusVocabulary──▶ bridge status ──statusMap──▶ site workflow status
53
+ // (reviewing) ("In Review") ("In Review")
54
+ //
55
+ // Unmapped statuses fall through unchanged at either layer. Neither file's meaning changes;
56
+ // this is only the stated relationship between them (two undocumented status mappings is a bug
57
+ // factory).
39
58
 
40
59
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
41
60
  import { join, dirname, resolve } from "node:path";
@@ -256,6 +275,68 @@ export const providers = {
256
275
  backlog: { requiresSync: false, project: projectBacklog },
257
276
  };
258
277
 
278
+ /* ── `.board.json` config schema (spec 054) — a SEPARATE, smaller table from `providers`
279
+ * above. `providers` is the projector registry (spec 052/056): provider name -> how to
280
+ * recompute the mirror. This table only knows the config-schema shape — which provider names
281
+ * are legal and which fields each requires — so `.board.json` can be validated today without
282
+ * pre-empting spec 056's ownership of adding `jira`'s projector entry to `providers`. Do NOT
283
+ * fold these two tables into one: that would re-couple config validation to projector
284
+ * implementation, which is exactly the coupling keeping them apart avoids. */
285
+ const BOARD_CONFIG_PROVIDERS = {
286
+ backlog: { requiredFields: [] },
287
+ jira: { requiredFields: ["cloudId", "projectKey", "issueTypeName"] },
288
+ };
289
+
290
+ /** Load `<root>/.board.json`. Returns `{ provider: "backlog" }` when absent — spec 053's
291
+ * backward-compatible default. Throws on malformed JSON (fail closed). Throws naming the known
292
+ * providers on an unknown `provider` value — NEVER falls back to `backlog`: silently treating
293
+ * a Jira project as a Backlog project is the exact silent no-op this feature exists to
294
+ * remove. */
295
+ export function loadBoardConfig(root) {
296
+ const path = join(root, ".board.json");
297
+ if (!existsSync(path)) return { provider: "backlog" };
298
+ const raw = readFileSync(path, "utf8");
299
+ let config;
300
+ try {
301
+ config = JSON.parse(raw);
302
+ } catch (e) {
303
+ throw new Error(`${path}: malformed JSON (${e.message})`);
304
+ }
305
+ const name = config?.provider;
306
+ if (!BOARD_CONFIG_PROVIDERS[name])
307
+ throw new Error(`${path}: unknown board provider ${JSON.stringify(name)} (known: ${Object.keys(BOARD_CONFIG_PROVIDERS).join(", ")})`);
308
+ return config;
309
+ }
310
+
311
+ /** Validate a `.board.json` object (as returned by `loadBoardConfig` or built in memory).
312
+ * Returns human-readable problems, empty when valid. Catches: `provider` as an array (one
313
+ * board is the plan of record — a list is a validation error saying why) or other non-string,
314
+ * an unknown provider name, a known provider missing one of its required fields (`jira` needs
315
+ * `cloudId`/`projectKey`/`issueTypeName`), and a non-object `statusMap`. */
316
+ export function validateBoardConfig(config) {
317
+ const problems = [];
318
+ if (!config || typeof config !== "object") {
319
+ problems.push("config: expected object");
320
+ return problems;
321
+ }
322
+ const name = config.provider;
323
+ if (Array.isArray(name)) {
324
+ problems.push("provider: expected a single string, got an array (one board is the plan of record)");
325
+ } else if (typeof name !== "string") {
326
+ problems.push(`provider: expected string, got ${name === undefined ? "missing" : typeof name}`);
327
+ } else if (!BOARD_CONFIG_PROVIDERS[name]) {
328
+ problems.push(`provider: unknown board provider "${name}" (known: ${Object.keys(BOARD_CONFIG_PROVIDERS).join(", ")})`);
329
+ } else {
330
+ const { requiredFields } = BOARD_CONFIG_PROVIDERS[name];
331
+ const sub = config[name] || {};
332
+ for (const field of requiredFields)
333
+ if (!sub[field]) problems.push(`${name}.${field}: required for provider "${name}"`);
334
+ if (sub.statusMap !== undefined && (typeof sub.statusMap !== "object" || Array.isArray(sub.statusMap) || sub.statusMap === null))
335
+ problems.push(`${name}.statusMap: expected object, got ${Array.isArray(sub.statusMap) ? "array" : typeof sub.statusMap}`);
336
+ }
337
+ return problems;
338
+ }
339
+
259
340
  /** Run git argv `args` in `cwd`. Never throws — a git failure is data, not an exception,
260
341
  * matching grounding-wiki/gates/repin-window.mjs's `git()` helper shape (spawnSync, argv
261
342
  * array so there is no shell, utf8 encoding). Returns `{ status, out }`; `status` is `null`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@praxisflux/gates",
3
- "version": "0.59.0",
3
+ "version": "0.59.3",
4
4
  "description": "praxisflux gate checks as a zero-dependency CLI (spec-bridge, wiki-freshness, course) — status can't exceed proven artifacts",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,6 +36,25 @@
36
36
  //
37
37
  // Dual-use: `node lib/board-mirror.mjs --check --root <dir>` mechanizes drift for a
38
38
  // deterministic provider and staleness for a model-backed one (see the CLI block below).
39
+ //
40
+ // `.board.json` (spec 054) is the OTHER file this module owns, and a different question from
41
+ // the mirror above: `.board/links.json` is BOARD STATE (what's linked, its status);
42
+ // `.board.json` is BOARD CONFIG (which provider a project uses, and that provider's
43
+ // coordinates). Tracked, hand-editable, at the project root, outside every marker — same
44
+ // posture as `.claude/model-tiers.json` (plant only when absent; doctrine points at the config;
45
+ // the config is what you edit, not something generated). `loadBoardConfig`/`validateBoardConfig`
46
+ // near the `providers` registry below read/check it.
47
+ //
48
+ // `.board.json`'s `statusMap` (bridge status -> site workflow status) composes with
49
+ // `.spec-bridge.json`'s `statusVocabulary` (derivation stage -> bridge status, bridge.mjs:69)
50
+ // at a DIFFERENT layer — the two do not merge, and this is the written precedence:
51
+ //
52
+ // derivation stage ──statusVocabulary──▶ bridge status ──statusMap──▶ site workflow status
53
+ // (reviewing) ("In Review") ("In Review")
54
+ //
55
+ // Unmapped statuses fall through unchanged at either layer. Neither file's meaning changes;
56
+ // this is only the stated relationship between them (two undocumented status mappings is a bug
57
+ // factory).
39
58
 
40
59
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs";
41
60
  import { join, dirname, resolve } from "node:path";
@@ -256,6 +275,68 @@ export const providers = {
256
275
  backlog: { requiresSync: false, project: projectBacklog },
257
276
  };
258
277
 
278
+ /* ── `.board.json` config schema (spec 054) — a SEPARATE, smaller table from `providers`
279
+ * above. `providers` is the projector registry (spec 052/056): provider name -> how to
280
+ * recompute the mirror. This table only knows the config-schema shape — which provider names
281
+ * are legal and which fields each requires — so `.board.json` can be validated today without
282
+ * pre-empting spec 056's ownership of adding `jira`'s projector entry to `providers`. Do NOT
283
+ * fold these two tables into one: that would re-couple config validation to projector
284
+ * implementation, which is exactly the coupling keeping them apart avoids. */
285
+ const BOARD_CONFIG_PROVIDERS = {
286
+ backlog: { requiredFields: [] },
287
+ jira: { requiredFields: ["cloudId", "projectKey", "issueTypeName"] },
288
+ };
289
+
290
+ /** Load `<root>/.board.json`. Returns `{ provider: "backlog" }` when absent — spec 053's
291
+ * backward-compatible default. Throws on malformed JSON (fail closed). Throws naming the known
292
+ * providers on an unknown `provider` value — NEVER falls back to `backlog`: silently treating
293
+ * a Jira project as a Backlog project is the exact silent no-op this feature exists to
294
+ * remove. */
295
+ export function loadBoardConfig(root) {
296
+ const path = join(root, ".board.json");
297
+ if (!existsSync(path)) return { provider: "backlog" };
298
+ const raw = readFileSync(path, "utf8");
299
+ let config;
300
+ try {
301
+ config = JSON.parse(raw);
302
+ } catch (e) {
303
+ throw new Error(`${path}: malformed JSON (${e.message})`);
304
+ }
305
+ const name = config?.provider;
306
+ if (!BOARD_CONFIG_PROVIDERS[name])
307
+ throw new Error(`${path}: unknown board provider ${JSON.stringify(name)} (known: ${Object.keys(BOARD_CONFIG_PROVIDERS).join(", ")})`);
308
+ return config;
309
+ }
310
+
311
+ /** Validate a `.board.json` object (as returned by `loadBoardConfig` or built in memory).
312
+ * Returns human-readable problems, empty when valid. Catches: `provider` as an array (one
313
+ * board is the plan of record — a list is a validation error saying why) or other non-string,
314
+ * an unknown provider name, a known provider missing one of its required fields (`jira` needs
315
+ * `cloudId`/`projectKey`/`issueTypeName`), and a non-object `statusMap`. */
316
+ export function validateBoardConfig(config) {
317
+ const problems = [];
318
+ if (!config || typeof config !== "object") {
319
+ problems.push("config: expected object");
320
+ return problems;
321
+ }
322
+ const name = config.provider;
323
+ if (Array.isArray(name)) {
324
+ problems.push("provider: expected a single string, got an array (one board is the plan of record)");
325
+ } else if (typeof name !== "string") {
326
+ problems.push(`provider: expected string, got ${name === undefined ? "missing" : typeof name}`);
327
+ } else if (!BOARD_CONFIG_PROVIDERS[name]) {
328
+ problems.push(`provider: unknown board provider "${name}" (known: ${Object.keys(BOARD_CONFIG_PROVIDERS).join(", ")})`);
329
+ } else {
330
+ const { requiredFields } = BOARD_CONFIG_PROVIDERS[name];
331
+ const sub = config[name] || {};
332
+ for (const field of requiredFields)
333
+ if (!sub[field]) problems.push(`${name}.${field}: required for provider "${name}"`);
334
+ if (sub.statusMap !== undefined && (typeof sub.statusMap !== "object" || Array.isArray(sub.statusMap) || sub.statusMap === null))
335
+ problems.push(`${name}.statusMap: expected object, got ${Array.isArray(sub.statusMap) ? "array" : typeof sub.statusMap}`);
336
+ }
337
+ return problems;
338
+ }
339
+
259
340
  /** Run git argv `args` in `cwd`. Never throws — a git failure is data, not an exception,
260
341
  * matching grounding-wiki/gates/repin-window.mjs's `git()` helper shape (spawnSync, argv
261
342
  * array so there is no shell, utf8 encoding). Returns `{ status, out }`; `status` is `null`