syntaur 0.47.0 → 0.48.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.
Files changed (72) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dashboard/dist/assets/{_basePickBy-DgR0_P-o.js → _basePickBy-Cie7FXcV.js} +1 -1
  3. package/dashboard/dist/assets/{_baseUniq-C8_Ych09.js → _baseUniq-C2af7-lW.js} +1 -1
  4. package/dashboard/dist/assets/{arc-yMHz4vGa.js → arc-BHKGjEv8.js} +1 -1
  5. package/dashboard/dist/assets/{architectureDiagram-2XIMDMQ5-ColWcH3P.js → architectureDiagram-2XIMDMQ5-CWqzcG1t.js} +1 -1
  6. package/dashboard/dist/assets/{blockDiagram-WCTKOSBZ-Bo8Npvfq.js → blockDiagram-WCTKOSBZ-CHLC0M63.js} +1 -1
  7. package/dashboard/dist/assets/{c4Diagram-IC4MRINW-B2ky8AT7.js → c4Diagram-IC4MRINW-Bx6GTZzJ.js} +1 -1
  8. package/dashboard/dist/assets/channel-CXlttLwe.js +1 -0
  9. package/dashboard/dist/assets/{chunk-4BX2VUAB-CyF6Z6dx.js → chunk-4BX2VUAB-7cTgwG6c.js} +1 -1
  10. package/dashboard/dist/assets/{chunk-55IACEB6-BJOEnwNN.js → chunk-55IACEB6-DQmdA20e.js} +1 -1
  11. package/dashboard/dist/assets/{chunk-FMBD7UC4-D3siQyQ4.js → chunk-FMBD7UC4-knmOnIKI.js} +1 -1
  12. package/dashboard/dist/assets/{chunk-JSJVCQXG-DKGuxEMf.js → chunk-JSJVCQXG-CaiLHJFn.js} +1 -1
  13. package/dashboard/dist/assets/{chunk-KX2RTZJC-CNIWWO2F.js → chunk-KX2RTZJC-Crp4zWuY.js} +1 -1
  14. package/dashboard/dist/assets/{chunk-NQ4KR5QH-DXt05c7h.js → chunk-NQ4KR5QH-DDjtdNaK.js} +1 -1
  15. package/dashboard/dist/assets/{chunk-QZHKN3VN-CM63uYnf.js → chunk-QZHKN3VN-Cbm1IieP.js} +1 -1
  16. package/dashboard/dist/assets/{chunk-WL4C6EOR-Dqvl_14m.js → chunk-WL4C6EOR-CdrRh6R4.js} +1 -1
  17. package/dashboard/dist/assets/classDiagram-VBA2DB6C-BJM0Dprk.js +1 -0
  18. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-BJM0Dprk.js +1 -0
  19. package/dashboard/dist/assets/clone-BTq7ueDt.js +1 -0
  20. package/dashboard/dist/assets/{cose-bilkent-S5V4N54A-WBLtT1w9.js → cose-bilkent-S5V4N54A-DyNajnMF.js} +1 -1
  21. package/dashboard/dist/assets/{dagre-KLK3FWXG-DIdQdwa7.js → dagre-KLK3FWXG-CIBjpfqE.js} +1 -1
  22. package/dashboard/dist/assets/{diagram-E7M64L7V-BEH6P_Sk.js → diagram-E7M64L7V-CDENlpZN.js} +1 -1
  23. package/dashboard/dist/assets/{diagram-IFDJBPK2-BuhxBcSy.js → diagram-IFDJBPK2-CIGMJoFB.js} +1 -1
  24. package/dashboard/dist/assets/{diagram-P4PSJMXO-DPSNVVzN.js → diagram-P4PSJMXO-C27fj5wp.js} +1 -1
  25. package/dashboard/dist/assets/{erDiagram-INFDFZHY-DYJb_rF5.js → erDiagram-INFDFZHY-M2CywJym.js} +1 -1
  26. package/dashboard/dist/assets/{flowDiagram-PKNHOUZH-B9_8BI26.js → flowDiagram-PKNHOUZH-CTV6ROYf.js} +1 -1
  27. package/dashboard/dist/assets/{ganttDiagram-A5KZAMGK-Bsg3QOhs.js → ganttDiagram-A5KZAMGK-CxH3f3e5.js} +1 -1
  28. package/dashboard/dist/assets/{gitGraphDiagram-K3NZZRJ6-Cf5G9x_K.js → gitGraphDiagram-K3NZZRJ6-CwhnVvHE.js} +1 -1
  29. package/dashboard/dist/assets/{graph-DyXfcrIH.js → graph-DGkI_ekZ.js} +1 -1
  30. package/dashboard/dist/assets/index-BxO2I5dN.css +1 -0
  31. package/dashboard/dist/assets/{index-C3kYxhbQ.js → index-CTdFARW9.js} +111 -111
  32. package/dashboard/dist/assets/{infoDiagram-LFFYTUFH-Bu1zlXs2.js → infoDiagram-LFFYTUFH-CuKReAi2.js} +1 -1
  33. package/dashboard/dist/assets/{ishikawaDiagram-PHBUUO56-fb8C-XRT.js → ishikawaDiagram-PHBUUO56-DyBMt5Mw.js} +1 -1
  34. package/dashboard/dist/assets/{journeyDiagram-4ABVD52K-smlBWs2O.js → journeyDiagram-4ABVD52K-CnwtbTNs.js} +1 -1
  35. package/dashboard/dist/assets/{kanban-definition-K7BYSVSG-Bz1AxFRE.js → kanban-definition-K7BYSVSG-5Sh1bC5j.js} +1 -1
  36. package/dashboard/dist/assets/{layout-VsTD3onG.js → layout-W2Vytcip.js} +1 -1
  37. package/dashboard/dist/assets/{linear-CE8xncGu.js → linear-UTm9E6c4.js} +1 -1
  38. package/dashboard/dist/assets/{mermaid.core-C0KQpDyW.js → mermaid.core-B0EB-dBk.js} +4 -4
  39. package/dashboard/dist/assets/{mindmap-definition-YRQLILUH-SRE5Immj.js → mindmap-definition-YRQLILUH-CH4L08eg.js} +1 -1
  40. package/dashboard/dist/assets/{pieDiagram-SKSYHLDU-CaZ_aCcD.js → pieDiagram-SKSYHLDU-CQ3PMl-w.js} +1 -1
  41. package/dashboard/dist/assets/{quadrantDiagram-337W2JSQ-Dd6MIruu.js → quadrantDiagram-337W2JSQ-DbZgXUm5.js} +1 -1
  42. package/dashboard/dist/assets/{requirementDiagram-Z7DCOOCP-BBXvP53l.js → requirementDiagram-Z7DCOOCP-CxJWytQa.js} +1 -1
  43. package/dashboard/dist/assets/{sankeyDiagram-WA2Y5GQK-DnS1SMIm.js → sankeyDiagram-WA2Y5GQK-3YeEasvh.js} +1 -1
  44. package/dashboard/dist/assets/{sequenceDiagram-2WXFIKYE-CLHJ1Uhx.js → sequenceDiagram-2WXFIKYE-F2EyvYsW.js} +1 -1
  45. package/dashboard/dist/assets/{stateDiagram-RAJIS63D-B6vrAeYw.js → stateDiagram-RAJIS63D-DrmiemaD.js} +1 -1
  46. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-DfyvP5EF.js +1 -0
  47. package/dashboard/dist/assets/{timeline-definition-YZTLITO2-BlHwGfnL.js → timeline-definition-YZTLITO2-CBee2YiE.js} +1 -1
  48. package/dashboard/dist/assets/{treemap-KZPCXAKY-D9kOGUYR.js → treemap-KZPCXAKY-BkPRIS3K.js} +1 -1
  49. package/dashboard/dist/assets/{vennDiagram-LZ73GAT5-BpQgeveT.js → vennDiagram-LZ73GAT5-CTsfwv8G.js} +1 -1
  50. package/dashboard/dist/assets/{xychartDiagram-JWTSCODW-DRch79fE.js → xychartDiagram-JWTSCODW-DUSo-c6w.js} +1 -1
  51. package/dashboard/dist/index.html +2 -2
  52. package/dist/dashboard/server.js +162 -68
  53. package/dist/dashboard/server.js.map +1 -1
  54. package/dist/index.js +162 -68
  55. package/dist/index.js.map +1 -1
  56. package/dist/launch/index.d.ts +25 -12
  57. package/dist/launch/index.js +60 -57
  58. package/dist/launch/index.js.map +1 -1
  59. package/package.json +1 -1
  60. package/platforms/claude-code/.claude-plugin/plugin.json +1 -1
  61. package/platforms/claude-code/skills/manage-statuses/SKILL.md +7 -0
  62. package/platforms/codex/.codex-plugin/plugin.json +1 -1
  63. package/platforms/codex/skills/manage-statuses/SKILL.md +7 -0
  64. package/platforms/hermes/plugins/syntaur/__pycache__/__init__.cpython-312.pyc +0 -0
  65. package/platforms/hermes/plugins/syntaur/__pycache__/boundary.cpython-312.pyc +0 -0
  66. package/skills/manage-statuses/SKILL.md +7 -0
  67. package/dashboard/dist/assets/channel-CUTEvTdk.js +0 -1
  68. package/dashboard/dist/assets/classDiagram-VBA2DB6C-Bkoc7orC.js +0 -1
  69. package/dashboard/dist/assets/classDiagram-v2-RAHNMMFH-Bkoc7orC.js +0 -1
  70. package/dashboard/dist/assets/clone-CltBg7cH.js +0 -1
  71. package/dashboard/dist/assets/index-DKr21dk8.css +0 -1
  72. package/dashboard/dist/assets/stateDiagram-v2-FVOUBMTO-BeqNZKbk.js +0 -1
@@ -3046,6 +3046,53 @@ function factFieldNames(decl) {
3046
3046
  ].map((k) => k.toLowerCase()) : [exportNames.fact.toLowerCase()];
3047
3047
  return { storageKey: name, exports: exportNames, registryKeys };
3048
3048
  }
3049
+ function validateFactDeclarations(raw2) {
3050
+ const problems = [];
3051
+ const owners = /* @__PURE__ */ new Map();
3052
+ for (const key of Object.keys(DERIVE_FIELDS)) owners.set(key, "built-in");
3053
+ for (const key of Object.keys(ASSIGNMENT_FIELDS)) owners.set(key, "built-in");
3054
+ for (const row of raw2 ?? []) {
3055
+ const name = (row?.name ?? "").trim();
3056
+ if (!/^[a-z][a-zA-Z0-9]*$/.test(name)) {
3057
+ problems.push(
3058
+ `fact "${row?.name ?? ""}": invalid name \u2014 must match /^[a-z][a-zA-Z0-9]*$/`
3059
+ );
3060
+ continue;
3061
+ }
3062
+ const type = (row.type ?? "").trim();
3063
+ if (type !== "bool" && type !== "number" && type !== "attestation") {
3064
+ problems.push(
3065
+ `fact "${name}": invalid type "${row.type ?? ""}" \u2014 expected bool, number, or attestation`
3066
+ );
3067
+ continue;
3068
+ }
3069
+ if (type === "attestation") {
3070
+ const binds = (row.binds ?? "none").toString().trim() || "none";
3071
+ if (binds !== "plan" && binds !== "commit" && binds !== "none") {
3072
+ problems.push(
3073
+ `fact "${name}": invalid binds "${row.binds}" \u2014 expected plan, commit, or none`
3074
+ );
3075
+ continue;
3076
+ }
3077
+ }
3078
+ const decl = type === "attestation" ? { name, type, binds: "none" } : { name, type };
3079
+ const keys = factFieldNames(decl).registryKeys;
3080
+ const collidingKey = keys.find((k) => owners.has(k));
3081
+ if (collidingKey !== void 0) {
3082
+ const owner = owners.get(collidingKey);
3083
+ if (owner === "built-in") {
3084
+ problems.push(`fact "${name}": exported field "${collidingKey}" collides with a built-in field`);
3085
+ } else if (owner === name) {
3086
+ problems.push(`fact "${name}": duplicate declaration (a fact named "${name}" is already declared)`);
3087
+ } else {
3088
+ problems.push(`fact "${name}": exported field "${collidingKey}" collides with fact "${owner}"`);
3089
+ }
3090
+ continue;
3091
+ }
3092
+ for (const key of keys) owners.set(key, name);
3093
+ }
3094
+ return problems;
3095
+ }
3049
3096
  function acceptFactDeclarations(declarations) {
3050
3097
  const taken = /* @__PURE__ */ new Set([
3051
3098
  ...Object.keys(DERIVE_FIELDS),
@@ -3571,7 +3618,6 @@ function parseStatusConfig(content) {
3571
3618
  continue;
3572
3619
  }
3573
3620
  }
3574
- if (statuses.length === 0) return null;
3575
3621
  const derive = phaseLadder.length > 0 || disposition.length > 0 || Object.keys(headline).length > 0 ? {
3576
3622
  phaseLadder: phaseLadder.length > 0 ? phaseLadder : DEFAULT_DERIVE_CONFIG.phaseLadder,
3577
3623
  disposition: disposition.length > 0 ? disposition : DEFAULT_DERIVE_CONFIG.disposition,
@@ -3582,6 +3628,7 @@ function parseStatusConfig(content) {
3582
3628
  active: "phase"
3583
3629
  }
3584
3630
  } : null;
3631
+ if (statuses.length === 0 && facts.length === 0 && derive === null) return null;
3585
3632
  return {
3586
3633
  statuses,
3587
3634
  order: order.length > 0 ? order : statuses.map((s) => s.id),
@@ -3707,53 +3754,6 @@ function validateDeriveConfig(derive, statusConfig, validateWhen = () => null) {
3707
3754
  }
3708
3755
  return problems;
3709
3756
  }
3710
- function validateFactDeclarations(raw2) {
3711
- const problems = [];
3712
- const owners = /* @__PURE__ */ new Map();
3713
- for (const key of Object.keys(DERIVE_FIELDS)) owners.set(key, "built-in");
3714
- for (const key of Object.keys(ASSIGNMENT_FIELDS)) owners.set(key, "built-in");
3715
- for (const row of raw2 ?? []) {
3716
- const name = (row?.name ?? "").trim();
3717
- if (!/^[a-z][a-zA-Z0-9]*$/.test(name)) {
3718
- problems.push(
3719
- `fact "${row?.name ?? ""}": invalid name \u2014 must match /^[a-z][a-zA-Z0-9]*$/`
3720
- );
3721
- continue;
3722
- }
3723
- const type = (row.type ?? "").trim();
3724
- if (type !== "bool" && type !== "number" && type !== "attestation") {
3725
- problems.push(
3726
- `fact "${name}": invalid type "${row.type ?? ""}" \u2014 expected bool, number, or attestation`
3727
- );
3728
- continue;
3729
- }
3730
- if (type === "attestation") {
3731
- const binds = (row.binds ?? "none").toString().trim() || "none";
3732
- if (binds !== "plan" && binds !== "commit" && binds !== "none") {
3733
- problems.push(
3734
- `fact "${name}": invalid binds "${row.binds}" \u2014 expected plan, commit, or none`
3735
- );
3736
- continue;
3737
- }
3738
- }
3739
- const decl = type === "attestation" ? { name, type, binds: "none" } : { name, type };
3740
- const keys = factFieldNames(decl).registryKeys;
3741
- const collidingKey = keys.find((k) => owners.has(k));
3742
- if (collidingKey !== void 0) {
3743
- const owner = owners.get(collidingKey);
3744
- if (owner === "built-in") {
3745
- problems.push(`fact "${name}": exported field "${collidingKey}" collides with a built-in field`);
3746
- } else if (owner === name) {
3747
- problems.push(`fact "${name}": duplicate declaration (a fact named "${name}" is already declared)`);
3748
- } else {
3749
- problems.push(`fact "${name}": exported field "${collidingKey}" collides with fact "${owner}"`);
3750
- }
3751
- continue;
3752
- }
3753
- for (const key of keys) owners.set(key, name);
3754
- }
3755
- return problems;
3756
- }
3757
3757
  function serializeIntegrationConfig(integrations) {
3758
3758
  const lines = [];
3759
3759
  if (integrations.claudePluginDir) {
@@ -4833,7 +4833,6 @@ var init_config2 = __esm({
4833
4833
  init_agents_schema();
4834
4834
  init_slug();
4835
4835
  init_fact_registry();
4836
- init_query();
4837
4836
  init_terminal_schema();
4838
4837
  init_workspace_visibility_schema();
4839
4838
  DEFAULT_DERIVE_CONFIG = {
@@ -7453,24 +7452,28 @@ async function getStatusConfig() {
7453
7452
  if (_cachedConfig) return _cachedConfig;
7454
7453
  const config = await readConfig();
7455
7454
  if (config.statuses) {
7455
+ const sc = config.statuses;
7456
+ const defaults = sc.statuses.length === 0 ? buildDefaultStatusConfig() : null;
7457
+ const effectiveStatuses = defaults ? defaults.statuses : sc.statuses;
7458
+ const effectiveOrder = defaults ? defaults.order : sc.order;
7456
7459
  const terminalSet = new Set(
7457
- config.statuses.statuses.filter((s) => s.terminal).map((s) => s.id)
7460
+ effectiveStatuses.filter((s) => s.terminal).map((s) => s.id)
7458
7461
  );
7459
- const hasCustomTransitions = config.statuses.transitions.length > 0;
7460
- const effectiveTransitions = hasCustomTransitions ? config.statuses.transitions : Array.from(DEFAULT_TRANSITION_TABLE.entries()).map(([key, to]) => {
7462
+ const hasCustomTransitions = sc.transitions.length > 0;
7463
+ const effectiveTransitions = hasCustomTransitions ? sc.transitions : Array.from(DEFAULT_TRANSITION_TABLE.entries()).map(([key, to]) => {
7461
7464
  const [from, command] = key.split(":");
7462
7465
  return { from, command, to };
7463
7466
  });
7464
- const accepted = acceptFactDeclarations(normalizeFactDeclarations(config.statuses.facts ?? null));
7467
+ const accepted = acceptFactDeclarations(normalizeFactDeclarations(sc.facts ?? null));
7465
7468
  _cachedConfig = {
7466
7469
  custom: true,
7467
- statuses: config.statuses.statuses,
7468
- order: config.statuses.order,
7470
+ statuses: effectiveStatuses,
7471
+ order: effectiveOrder,
7469
7472
  transitions: effectiveTransitions,
7470
7473
  transitionTable: buildTransitionTable(effectiveTransitions),
7471
7474
  terminalStatuses: terminalSet.size > 0 ? terminalSet : /* @__PURE__ */ new Set(["completed", "failed"]),
7472
- derive: config.statuses.derive ?? null,
7473
- facts: config.statuses.facts ?? null,
7475
+ derive: sc.derive ?? null,
7476
+ facts: sc.facts ?? null,
7474
7477
  factDeclarations: accepted,
7475
7478
  deriveRegistry: buildDeriveRegistry(accepted),
7476
7479
  queryRegistry: buildQueryRegistry(accepted)
@@ -8634,7 +8637,7 @@ async function buildDerivedDetail(assignment, assignmentDir, projectDir) {
8634
8637
  try {
8635
8638
  const { computeFactsDetailed: computeFactsDetailed2 } = await Promise.resolve().then(() => (init_facts(), facts_exports));
8636
8639
  const { deriveDimensions: deriveDimensions2 } = await Promise.resolve().then(() => (init_derive(), derive_exports));
8637
- const { DEFAULT_DERIVE_CONFIG: DEFAULT_DERIVE_CONFIG2 } = await Promise.resolve().then(() => (init_config2(), config_exports));
8640
+ const { DEFAULT_DERIVE_CONFIG: DEFAULT_DERIVE_CONFIG3 } = await Promise.resolve().then(() => (init_config2(), config_exports));
8638
8641
  const { facts, attestations } = await computeFactsDetailed2({
8639
8642
  assignmentDir,
8640
8643
  frontmatter: {
@@ -8649,7 +8652,7 @@ async function buildDerivedDetail(assignment, assignmentDir, projectDir) {
8649
8652
  });
8650
8653
  const dims = deriveDimensions2({
8651
8654
  facts,
8652
- derive: config.derive ?? DEFAULT_DERIVE_CONFIG2,
8655
+ derive: config.derive ?? DEFAULT_DERIVE_CONFIG3,
8653
8656
  currentStatus: assignment.status,
8654
8657
  terminalStatuses: config.terminalStatuses,
8655
8658
  knownStatusIds: new Set(config.statuses.map((s) => s.id)),
@@ -16713,7 +16716,9 @@ function createWorkspaceVisibilityConfigRouter() {
16713
16716
 
16714
16717
  // src/dashboard/api-status-config.ts
16715
16718
  init_config2();
16719
+ init_fact_registry();
16716
16720
  init_api();
16721
+ init_derive();
16717
16722
  import { Router as Router9 } from "express";
16718
16723
 
16719
16724
  // src/utils/status-config-resolution.ts
@@ -17090,7 +17095,8 @@ function createStatusConfigRouter(projectsDir, assignmentsDir2) {
17090
17095
  order: config.order,
17091
17096
  transitions: config.transitions,
17092
17097
  custom: config.custom,
17093
- factDeclarations: config.factDeclarations
17098
+ factDeclarations: config.factDeclarations,
17099
+ rawFacts: config.facts ?? []
17094
17100
  });
17095
17101
  } catch (error) {
17096
17102
  console.error("Error getting status config:", error);
@@ -17114,8 +17120,27 @@ function createStatusConfigRouter(projectsDir, assignmentsDir2) {
17114
17120
  });
17115
17121
  router.post("/", async (req, res) => {
17116
17122
  try {
17117
- const { statuses, order, transitions, resolutions: rawResolutions } = req.body ?? {};
17118
- if (!Array.isArray(statuses) || !Array.isArray(order) || !Array.isArray(transitions)) {
17123
+ const {
17124
+ statuses,
17125
+ order,
17126
+ transitions,
17127
+ facts: bodyFacts,
17128
+ factRemovalAcks,
17129
+ resolutions: rawResolutions
17130
+ } = req.body ?? {};
17131
+ const currentConfig = await getStatusConfig();
17132
+ const hasFacts = bodyFacts !== void 0;
17133
+ const hasStatuses = statuses !== void 0;
17134
+ const hasOrder = order !== void 0;
17135
+ const hasTransitions = transitions !== void 0;
17136
+ let effectiveStatuses = statuses;
17137
+ let effectiveOrder = order;
17138
+ let effectiveTransitions = transitions;
17139
+ if (hasFacts && !hasStatuses && !hasOrder && !hasTransitions) {
17140
+ effectiveStatuses = currentConfig.statuses;
17141
+ effectiveOrder = currentConfig.order;
17142
+ effectiveTransitions = currentConfig.transitions;
17143
+ } else if (!Array.isArray(effectiveStatuses) || !Array.isArray(effectiveOrder) || !Array.isArray(effectiveTransitions)) {
17119
17144
  res.status(400).json({ error: "malformed-statuses", message: "Request body must include statuses, order, and transitions arrays" });
17120
17145
  return;
17121
17146
  }
@@ -17129,10 +17154,9 @@ function createStatusConfigRouter(projectsDir, assignmentsDir2) {
17129
17154
  return;
17130
17155
  }
17131
17156
  const resolutions = parsed.resolutions;
17132
- const currentConfig = await getStatusConfig();
17133
17157
  const oldIds = new Set(currentConfig.statuses.map((s) => s.id));
17134
17158
  const newIds = /* @__PURE__ */ new Set();
17135
- for (const s of statuses) {
17159
+ for (const s of effectiveStatuses) {
17136
17160
  if (s && typeof s === "object" && isString(s.id)) {
17137
17161
  newIds.add(s.id);
17138
17162
  }
@@ -17213,13 +17237,83 @@ function createStatusConfigRouter(projectsDir, assignmentsDir2) {
17213
17237
  }
17214
17238
  throw err;
17215
17239
  }
17240
+ let factsToWrite = currentConfig.facts ?? null;
17241
+ if (hasFacts) {
17242
+ const shapedFacts = [];
17243
+ if (!Array.isArray(bodyFacts)) {
17244
+ res.status(400).json({ error: "malformed-facts", message: "facts must be an array" });
17245
+ return;
17246
+ }
17247
+ for (let i = 0; i < bodyFacts.length; i++) {
17248
+ const row = bodyFacts[i];
17249
+ if (!row || typeof row !== "object") {
17250
+ res.status(400).json({ error: "malformed-facts", message: `facts[${i}] must be an object` });
17251
+ return;
17252
+ }
17253
+ const name = row.name;
17254
+ const type = row.type;
17255
+ if (typeof name !== "string" || typeof type !== "string") {
17256
+ res.status(400).json({ error: "malformed-facts", message: `facts[${i}] must have name and type strings` });
17257
+ return;
17258
+ }
17259
+ const binds = row.binds;
17260
+ const normalizedBinds = binds === void 0 ? null : binds === null ? null : typeof binds === "string" ? binds : null;
17261
+ shapedFacts.push({ name, type, binds: normalizedBinds });
17262
+ }
17263
+ const problems = validateFactDeclarations(shapedFacts);
17264
+ if (problems.length > 0) {
17265
+ res.status(400).json({ error: "invalid-facts", problems });
17266
+ return;
17267
+ }
17268
+ const currentNames = new Set((currentConfig.facts ?? []).map((f) => f.name));
17269
+ const incomingNames = new Set(shapedFacts.map((f) => f.name));
17270
+ const removedNames = [];
17271
+ for (const name of currentNames) {
17272
+ if (!incomingNames.has(name)) removedNames.push(name);
17273
+ }
17274
+ const acks = new Set(Array.isArray(factRemovalAcks) ? factRemovalAcks.map((x) => String(x)) : []);
17275
+ const unresolvedRefs = [];
17276
+ const deriveConfig = currentConfig.derive ?? null;
17277
+ if (deriveConfig !== null && removedNames.length > 0) {
17278
+ const acceptedAll = acceptFactDeclarations(normalizeFactDeclarations(currentConfig.facts));
17279
+ const fullRegistry = buildDeriveRegistry(acceptedAll);
17280
+ for (const removedName of removedNames) {
17281
+ if (acks.has(removedName)) continue;
17282
+ const acceptedWithout = acceptedAll.filter((d) => d.name !== removedName);
17283
+ const withoutRegistry = buildDeriveRegistry(acceptedWithout);
17284
+ for (let i = 0; i < deriveConfig.phaseLadder.length; i++) {
17285
+ const rung = deriveConfig.phaseLadder[i];
17286
+ if (rung.when === "*") continue;
17287
+ const before = validateDeriveCondition(rung.when, fullRegistry);
17288
+ const after = validateDeriveCondition(rung.when, withoutRegistry);
17289
+ if (before === null && after !== null) {
17290
+ unresolvedRefs.push({ factName: removedName, location: `phaseLadder[${i}]`, when: rung.when });
17291
+ }
17292
+ }
17293
+ for (let i = 0; i < deriveConfig.disposition.length; i++) {
17294
+ const rule = deriveConfig.disposition[i];
17295
+ if (rule.when === null) continue;
17296
+ const before = validateDeriveCondition(rule.when, fullRegistry);
17297
+ const after = validateDeriveCondition(rule.when, withoutRegistry);
17298
+ if (before === null && after !== null) {
17299
+ unresolvedRefs.push({ factName: removedName, location: `disposition[${i}]`, when: rule.when });
17300
+ }
17301
+ }
17302
+ }
17303
+ }
17304
+ if (unresolvedRefs.length > 0) {
17305
+ res.status(409).json({ error: "unresolved-fact-references", references: unresolvedRefs });
17306
+ return;
17307
+ }
17308
+ factsToWrite = shapedFacts;
17309
+ }
17216
17310
  try {
17217
17311
  await writeStatusConfig({
17218
- statuses,
17219
- order,
17220
- transitions,
17312
+ statuses: effectiveStatuses,
17313
+ order: effectiveOrder,
17314
+ transitions: effectiveTransitions,
17221
17315
  derive: currentConfig.derive ?? null,
17222
- facts: currentConfig.facts ?? null
17316
+ facts: factsToWrite
17223
17317
  });
17224
17318
  } catch (err) {
17225
17319
  console.error("Error saving status config after applying resolutions:", err);