@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`
|
package/lib/board-mirror.mjs
CHANGED
|
@@ -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
|
@@ -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`
|