@transitrix/diagrams 1.0.0 → 1.0.1

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 (267) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +47 -47
  3. package/dist/__tests__/edge-path.test.js +8 -0
  4. package/dist/activity-card/__tests__/example.test.js +20 -1
  5. package/dist/activity-card/__tests__/layout.test.js +102 -11
  6. package/dist/activity-card/__tests__/resolver.test.js +139 -6
  7. package/dist/activity-card/index.d.ts +1 -1
  8. package/dist/activity-card/layout.d.ts +1 -1
  9. package/dist/activity-card/layout.js +220 -103
  10. package/dist/activity-card/resolver.js +107 -26
  11. package/dist/activity-card/types.d.ts +150 -27
  12. package/dist/activity-card/validate.js +3 -0
  13. package/dist/actor/__tests__/validate.test.js +63 -31
  14. package/dist/actor/index.d.ts +1 -1
  15. package/dist/actor/index.js +1 -1
  16. package/dist/actor/types.d.ts +7 -3
  17. package/dist/actor/types.js +17 -3
  18. package/dist/actor/validate.js +55 -26
  19. package/dist/assertion/types.d.ts +3 -1
  20. package/dist/assertion/types.js +1 -1
  21. package/dist/change/__tests__/validate.test.js +40 -34
  22. package/dist/change/index.d.ts +1 -0
  23. package/dist/change/types.d.ts +1 -1
  24. package/dist/change/types.js +2 -2
  25. package/dist/change/validate.d.ts +6 -1
  26. package/dist/change/validate.js +68 -41
  27. package/dist/compliance/__tests__/classify.test.js +34 -0
  28. package/dist/compliance/__tests__/coverage-metric.test.js +146 -46
  29. package/dist/compliance/__tests__/html.test.js +2 -2
  30. package/dist/compliance/__tests__/impact.test.js +311 -6
  31. package/dist/compliance/classify.d.ts +6 -0
  32. package/dist/compliance/classify.js +9 -1
  33. package/dist/compliance/coverage-metric.d.ts +31 -11
  34. package/dist/compliance/coverage-metric.js +149 -39
  35. package/dist/compliance/html.js +77 -74
  36. package/dist/compliance/impact.d.ts +77 -16
  37. package/dist/compliance/impact.js +193 -25
  38. package/dist/compliance/index.d.ts +2 -2
  39. package/dist/compliance/index.js +1 -1
  40. package/dist/compliance/markdown.js +1 -0
  41. package/dist/compliance/types.d.ts +25 -6
  42. package/dist/edge-path.d.ts +8 -4
  43. package/dist/edge-path.js +11 -6
  44. package/dist/factor/__tests__/validate.test.js +74 -45
  45. package/dist/factor/index.d.ts +3 -2
  46. package/dist/factor/index.js +1 -1
  47. package/dist/factor/types.d.ts +4 -7
  48. package/dist/factor/types.js +10 -5
  49. package/dist/factor/validate.d.ts +6 -1
  50. package/dist/factor/validate.js +68 -50
  51. package/dist/fgca/__tests__/resolver.test.js +185 -0
  52. package/dist/fgca/index.d.ts +2 -0
  53. package/dist/fgca/index.js +1 -0
  54. package/dist/fgca/resolver.d.ts +49 -0
  55. package/dist/fgca/resolver.js +124 -0
  56. package/dist/goals/index.d.ts +1 -1
  57. package/dist/index.d.ts +5 -0
  58. package/dist/index.js +5 -0
  59. package/dist/process-blueprint/__tests__/layout.test.js +37 -0
  60. package/dist/process-blueprint/layout.js +5 -1
  61. package/dist/process-blueprint/types.d.ts +22 -1
  62. package/dist/stakeholder/__tests__/validate.test.js +49 -47
  63. package/dist/stakeholder/index.d.ts +1 -0
  64. package/dist/stakeholder/types.d.ts +3 -4
  65. package/dist/stakeholder/types.js +2 -3
  66. package/dist/stakeholder/validate.d.ts +6 -1
  67. package/dist/stakeholder/validate.js +71 -50
  68. package/dist/target-state/__tests__/validate.test.js +43 -27
  69. package/dist/target-state/index.d.ts +2 -0
  70. package/dist/target-state/index.js +1 -0
  71. package/dist/target-state/types.d.ts +6 -1
  72. package/dist/target-state/types.js +8 -7
  73. package/dist/target-state/validate.d.ts +6 -1
  74. package/dist/target-state/validate.js +69 -45
  75. package/dist/theme/themes.js +69 -69
  76. package/dist/webview/__tests__/entry.test.js +11 -11
  77. package/dist/webview/render-activities.d.ts +3 -1
  78. package/dist/webview/render-activities.js +2 -2
  79. package/dist/webview/render-activity-card.js +104 -77
  80. package/dist/webview/render-fgca.d.ts +4 -0
  81. package/dist/webview/render-fgca.js +2 -5
  82. package/dist/webview/render-goals.d.ts +1 -0
  83. package/dist/webview/render-goals.js +15 -15
  84. package/dist/webview/render-process-blueprint.js +10 -10
  85. package/package.json +40 -40
  86. package/src/__tests__/edge-path.test.ts +58 -46
  87. package/src/__tests__/scope.test.ts +27 -27
  88. package/src/__tests__/yaml-normalize.test.ts +158 -158
  89. package/src/activities/__tests__/cpm.test.ts +164 -164
  90. package/src/activities/__tests__/gantt.test.ts +245 -245
  91. package/src/activities/__tests__/layout.test.ts +112 -112
  92. package/src/activities/__tests__/validate.test.ts +409 -409
  93. package/src/activities/cpm.ts +95 -95
  94. package/src/activities/gantt.ts +276 -276
  95. package/src/activities/index.ts +28 -28
  96. package/src/activities/layout.ts +112 -112
  97. package/src/activities/types.ts +148 -148
  98. package/src/activities/validate.ts +357 -357
  99. package/src/activity-card/__tests__/example.test.ts +85 -66
  100. package/src/activity-card/__tests__/layout.test.ts +169 -63
  101. package/src/activity-card/__tests__/resolver.test.ts +307 -161
  102. package/src/activity-card/__tests__/validate.test.ts +131 -131
  103. package/src/activity-card/index.ts +38 -32
  104. package/src/activity-card/layout.ts +324 -194
  105. package/src/activity-card/resolver.ts +415 -337
  106. package/src/activity-card/types.ts +376 -237
  107. package/src/activity-card/validate.ts +162 -158
  108. package/src/actor/__tests__/validate.test.ts +70 -31
  109. package/src/actor/index.ts +1 -1
  110. package/src/actor/types.ts +21 -7
  111. package/src/actor/validate.ts +59 -27
  112. package/src/applications/__tests__/validate.test.ts +256 -256
  113. package/src/applications/index.ts +11 -11
  114. package/src/applications/types.ts +43 -43
  115. package/src/applications/validate.ts +131 -131
  116. package/src/assertion/__tests__/example.test.ts +37 -37
  117. package/src/assertion/__tests__/validate.test.ts +142 -142
  118. package/src/assertion/index.ts +12 -12
  119. package/src/assertion/types.ts +60 -58
  120. package/src/assertion/validate.ts +145 -145
  121. package/src/blocks/__tests__/layout.test.ts +169 -169
  122. package/src/blocks/__tests__/validate.test.ts +260 -260
  123. package/src/blocks/index.ts +17 -17
  124. package/src/blocks/layout.ts +186 -186
  125. package/src/blocks/types.ts +62 -62
  126. package/src/blocks/validate.ts +206 -206
  127. package/src/capability-map/__tests__/validate.test.ts +238 -238
  128. package/src/capability-map/index.ts +8 -8
  129. package/src/capability-map/types.ts +29 -29
  130. package/src/capability-map/validate.ts +127 -127
  131. package/src/change/__tests__/validate.test.ts +43 -34
  132. package/src/change/index.ts +1 -0
  133. package/src/change/types.ts +5 -3
  134. package/src/change/validate.ts +75 -43
  135. package/src/compliance/__tests__/classify.test.ts +64 -27
  136. package/src/compliance/__tests__/confidence.test.ts +98 -98
  137. package/src/compliance/__tests__/coverage-metric.test.ts +300 -188
  138. package/src/compliance/__tests__/demo-scenario.test.ts +273 -273
  139. package/src/compliance/__tests__/example.test.ts +57 -57
  140. package/src/compliance/__tests__/gap-report.test.ts +128 -128
  141. package/src/compliance/__tests__/html.test.ts +141 -141
  142. package/src/compliance/__tests__/impact.test.ts +840 -481
  143. package/src/compliance/__tests__/markdown.test.ts +94 -94
  144. package/src/compliance/__tests__/views.test.ts +64 -64
  145. package/src/compliance/classify.ts +100 -84
  146. package/src/compliance/confidence.ts +69 -69
  147. package/src/compliance/coverage-metric.ts +404 -263
  148. package/src/compliance/gap-report.ts +94 -94
  149. package/src/compliance/html.ts +298 -295
  150. package/src/compliance/impact.ts +861 -677
  151. package/src/compliance/index.ts +57 -56
  152. package/src/compliance/markdown.ts +136 -135
  153. package/src/compliance/reverse-index.ts +33 -33
  154. package/src/compliance/types.ts +136 -120
  155. package/src/compliance/views.ts +46 -46
  156. package/src/compliance-matrix/__tests__/build.test.ts +200 -200
  157. package/src/compliance-matrix/__tests__/example.test.ts +57 -57
  158. package/src/compliance-matrix/index.ts +11 -11
  159. package/src/compliance-matrix/types.ts +102 -102
  160. package/src/edge-path.ts +50 -44
  161. package/src/factor/__tests__/validate.test.ts +80 -45
  162. package/src/factor/index.ts +3 -7
  163. package/src/factor/types.ts +14 -18
  164. package/src/factor/validate.ts +77 -51
  165. package/src/fgca/__tests__/chain-table.test.ts +172 -172
  166. package/src/fgca/__tests__/layout.test.ts +179 -179
  167. package/src/fgca/__tests__/parse-canonical.test.ts +248 -248
  168. package/src/fgca/__tests__/preview-layout.test.ts +124 -124
  169. package/src/fgca/__tests__/resolver.test.ts +209 -0
  170. package/src/fgca/__tests__/validate.test.ts +142 -142
  171. package/src/fgca/chain-table.ts +225 -225
  172. package/src/fgca/index.ts +47 -45
  173. package/src/fgca/layout.ts +204 -204
  174. package/src/fgca/nodeTypes.ts +11 -11
  175. package/src/fgca/nodes/FGCAActivityNode.tsx +65 -65
  176. package/src/fgca/nodes/FGCAChangeNode.tsx +64 -64
  177. package/src/fgca/nodes/FGCAFactorNode.tsx +64 -64
  178. package/src/fgca/nodes/FGCAGoalNode.tsx +72 -72
  179. package/src/fgca/parse-canonical.ts +400 -400
  180. package/src/fgca/preview-layout.ts +217 -217
  181. package/src/fgca/resolver.ts +157 -0
  182. package/src/fgca/types.ts +50 -50
  183. package/src/fgca/validate.ts +126 -126
  184. package/src/geometry.ts +9 -9
  185. package/src/goals/__tests__/layout.test.ts +175 -175
  186. package/src/goals/__tests__/mutations.test.ts +111 -111
  187. package/src/goals/__tests__/parse-canonical.test.ts +145 -145
  188. package/src/goals/__tests__/validate.test.ts +202 -202
  189. package/src/goals/index.ts +20 -21
  190. package/src/goals/layout.ts +172 -172
  191. package/src/goals/mutations.ts +123 -123
  192. package/src/goals/parse-canonical.ts +286 -286
  193. package/src/goals/types.ts +85 -85
  194. package/src/goals/validate.ts +136 -136
  195. package/src/index.ts +28 -23
  196. package/src/process-blueprint/__tests__/compliance-lane.test.ts +320 -320
  197. package/src/process-blueprint/__tests__/layout.test.ts +240 -195
  198. package/src/process-blueprint/__tests__/validate.test.ts +310 -310
  199. package/src/process-blueprint/index.ts +31 -31
  200. package/src/process-blueprint/layout.ts +535 -531
  201. package/src/process-blueprint/types.ts +263 -242
  202. package/src/process-blueprint/validate.ts +181 -181
  203. package/src/process-map/__tests__/validate.test.ts +223 -223
  204. package/src/process-map/index.ts +10 -10
  205. package/src/process-map/types.ts +36 -36
  206. package/src/process-map/validate.ts +135 -135
  207. package/src/products/__tests__/validate.test.ts +239 -239
  208. package/src/products/index.ts +9 -9
  209. package/src/products/types.ts +31 -31
  210. package/src/products/validate.ts +112 -112
  211. package/src/repo-validate/__tests__/validate-repo.test.ts +149 -149
  212. package/src/repo-validate/index.ts +2 -2
  213. package/src/repo-validate/types.ts +50 -50
  214. package/src/repo-validate/validate-repo.ts +196 -196
  215. package/src/requirement/__tests__/example.test.ts +24 -24
  216. package/src/requirement/__tests__/validate.test.ts +96 -96
  217. package/src/requirement/index.ts +4 -4
  218. package/src/requirement/types.ts +35 -35
  219. package/src/requirement/validate.ts +92 -92
  220. package/src/scenarios/__tests__/validate.test.ts +209 -209
  221. package/src/scenarios/index.ts +15 -15
  222. package/src/scenarios/types.ts +37 -37
  223. package/src/scenarios/validate.ts +122 -122
  224. package/src/schema-version.ts +16 -16
  225. package/src/scope.ts +46 -46
  226. package/src/stakeholder/__tests__/validate.test.ts +51 -47
  227. package/src/stakeholder/index.ts +1 -0
  228. package/src/stakeholder/types.ts +7 -7
  229. package/src/stakeholder/validate.ts +79 -52
  230. package/src/target-state/__tests__/validate.test.ts +48 -27
  231. package/src/target-state/index.ts +2 -0
  232. package/src/target-state/types.ts +11 -7
  233. package/src/target-state/validate.ts +76 -46
  234. package/src/theme/index.ts +4 -4
  235. package/src/theme/themes.ts +237 -237
  236. package/src/theme/tokens.ts +133 -133
  237. package/src/typed-id.ts +56 -56
  238. package/src/validation-types.ts +36 -36
  239. package/src/webview/__tests__/entry-notations.test.ts +77 -77
  240. package/src/webview/__tests__/entry.test.ts +64 -64
  241. package/src/webview/__tests__/render-goals.test.ts +72 -72
  242. package/src/webview/entry.ts +292 -292
  243. package/src/webview/render-activities.ts +5 -3
  244. package/src/webview/render-activity-card.ts +266 -234
  245. package/src/webview/render-fgca.ts +6 -5
  246. package/src/webview/render-goals.ts +111 -110
  247. package/src/webview/render-process-blueprint.ts +207 -207
  248. package/src/webview/styles.css +322 -323
  249. package/src/yaml-normalize.ts +56 -56
  250. package/dist/element-envelope.d.ts +0 -15
  251. package/dist/element-envelope.js +0 -55
  252. package/dist/issues/__tests__/layout.test.js +0 -62
  253. package/dist/issues/__tests__/validate.test.d.ts +0 -1
  254. package/dist/issues/__tests__/validate.test.js +0 -82
  255. package/dist/issues/index.d.ts +0 -4
  256. package/dist/issues/index.js +0 -2
  257. package/dist/issues/layout.d.ts +0 -10
  258. package/dist/issues/layout.js +0 -82
  259. package/dist/issues/types.d.ts +0 -61
  260. package/dist/issues/types.js +0 -6
  261. package/dist/issues/validate.d.ts +0 -3
  262. package/dist/issues/validate.js +0 -143
  263. package/dist/webview/transitrix-render.css +0 -323
  264. package/dist/webview/transitrix-render.js +0 -6452
  265. package/dist/webview/transitrix-render.js.map +0 -7
  266. package/src/element-envelope.ts +0 -76
  267. /package/dist/{issues/__tests__/layout.test.d.ts → fgca/__tests__/resolver.test.d.ts} +0 -0
@@ -16,9 +16,9 @@ import { buildComplianceIndex } from './reverse-index.js';
16
16
  * are auditable ("what I assumed").
17
17
  */
18
18
  export const COMPLIANCE_IMPACT_DEFAULTS = {
19
- /** Accept all four active statuses + n_a in cell aggregation. */
19
+ /** Accept all active statuses + n_a in cell aggregation. */
20
20
  status_display: {
21
- show: ['compliant', 'partial', 'non_compliant', 'under_review', 'n_a'],
21
+ show: ['compliant', 'partial', 'non_compliant', 'under_review', 'pending_owner', 'n_a'],
22
22
  },
23
23
  /** Order rows by canonical REQUIREMENT ID (lexicographic). */
24
24
  order_rows_by: 'id',
@@ -30,9 +30,9 @@ export const COMPLIANCE_IMPACT_DEFAULTS = {
30
30
  /** Obligation scope: all known REQUIREMENTs in the canon (no filter). */
31
31
  obligations: { include: undefined, filter: undefined },
32
32
  /** Subjects: empty — must be supplied explicitly in the view config. */
33
- subjects: { products: [], processes: [] },
34
- /** Column grain: one column per business object (no stage decomposition). */
35
- grouping: { columns: 'object' },
33
+ subjects: { products: [], processes: [], capabilities: [] },
34
+ /** Column grain: one column per subject (no stage/task decomposition). */
35
+ grouping: { columns: 'product' },
36
36
  };
37
37
  /**
38
38
  * Validate and normalise a raw (YAML-parsed) value into an ImpactViewConfig.
@@ -81,6 +81,16 @@ export function parseImpactViewConfig(raw) {
81
81
  const emptyCells = v.empty_cells && typeof v.empty_cells === 'object' && !Array.isArray(v.empty_cells)
82
82
  ? v.empty_cells
83
83
  : {};
84
+ // Parse grouping.columns — accept spec-canonical values plus backward-compat aliases.
85
+ const rawGrouping = v.grouping && typeof v.grouping === 'object' && !Array.isArray(v.grouping)
86
+ ? v.grouping
87
+ : null;
88
+ const rawColumns = typeof rawGrouping?.columns === 'string' ? rawGrouping.columns : undefined;
89
+ const VALID_COLUMNS = new Set(['product', 'product-stage', 'product-stage-task']);
90
+ const normalizedColumns = rawColumns === 'object-details' ? 'product-stage' // backward-compat alias
91
+ : rawColumns === 'object' ? 'product' // backward-compat alias
92
+ : VALID_COLUMNS.has(rawColumns ?? '') ? rawColumns
93
+ : undefined;
84
94
  const config = {
85
95
  id: v.id,
86
96
  name: v.name,
@@ -93,6 +103,9 @@ export function parseImpactViewConfig(raw) {
93
103
  processes: Array.isArray(subjects.processes)
94
104
  ? subjects.processes.filter((x) => typeof x === 'string')
95
105
  : [...COMPLIANCE_IMPACT_DEFAULTS.subjects.processes],
106
+ capabilities: Array.isArray(subjects.capabilities)
107
+ ? subjects.capabilities.filter((x) => typeof x === 'string')
108
+ : [...COMPLIANCE_IMPACT_DEFAULTS.subjects.capabilities],
96
109
  },
97
110
  obligations: {
98
111
  include: Array.isArray(obligations.include)
@@ -120,6 +133,7 @@ export function parseImpactViewConfig(raw) {
120
133
  : COMPLIANCE_IMPACT_DEFAULTS.empty_cells.no_obligation_applies_label,
121
134
  },
122
135
  order_rows_by: v.order_rows_by === 'name' ? 'name' : COMPLIANCE_IMPACT_DEFAULTS.order_rows_by,
136
+ grouping: normalizedColumns !== undefined ? { columns: normalizedColumns } : undefined,
123
137
  };
124
138
  return { ok: true, config };
125
139
  }
@@ -182,9 +196,11 @@ function orderRows(rows, key) {
182
196
  return sorted;
183
197
  }
184
198
  /** Aggregate multiple matching assertions into one cell value, per §5.2 step 4.
199
+ * Precedence: non_compliant > partial > pending_owner > under_review > compliant > n_a.
185
200
  * Assumes at least one assertion matches an allowed status; caller filters. */
186
201
  function aggregateStatus(assertions) {
187
202
  let sawPartial = false;
203
+ let sawPendingOwner = false;
188
204
  let sawUnderReview = false;
189
205
  let sawCompliant = false;
190
206
  for (const a of assertions) {
@@ -192,6 +208,8 @@ function aggregateStatus(assertions) {
192
208
  return 'non_compliant';
193
209
  if (a.status === 'partial')
194
210
  sawPartial = true;
211
+ else if (a.status === 'pending_owner')
212
+ sawPendingOwner = true;
195
213
  else if (a.status === 'under_review')
196
214
  sawUnderReview = true;
197
215
  else if (a.status === 'compliant')
@@ -199,6 +217,8 @@ function aggregateStatus(assertions) {
199
217
  }
200
218
  if (sawPartial)
201
219
  return 'partial';
220
+ if (sawPendingOwner)
221
+ return 'pending_owner';
202
222
  if (sawUnderReview)
203
223
  return 'under_review';
204
224
  if (sawCompliant)
@@ -206,13 +226,22 @@ function aggregateStatus(assertions) {
206
226
  return 'n_a';
207
227
  }
208
228
  // ── Column builder helpers (CV-3a) ──────────────────────────────────────────
209
- function buildProductColumns(config) {
210
- const subjects = [...(config.subjects.products ?? []), ...(config.subjects.processes ?? [])];
211
- return subjects.map(id => ({ subjectId: id, label: id }));
229
+ function buildProductColumns(config, namedItems) {
230
+ const nameMap = new Map(namedItems.map(p => [p.id, p.name]));
231
+ const subjects = [
232
+ ...(config.subjects.products ?? []),
233
+ ...(config.subjects.processes ?? []),
234
+ ...(config.subjects.capabilities ?? []),
235
+ ];
236
+ return subjects.map(id => ({ subjectId: id, label: id, subjectName: nameMap.get(id) }));
212
237
  }
213
238
  function buildObjectDetailColumns(config, objectDetails) {
214
239
  const detailMap = new Map(objectDetails.map(d => [d.objectId, d.details]));
215
- const subjects = [...(config.subjects.products ?? []), ...(config.subjects.processes ?? [])];
240
+ const subjects = [
241
+ ...(config.subjects.products ?? []),
242
+ ...(config.subjects.processes ?? []),
243
+ ...(config.subjects.capabilities ?? []),
244
+ ];
216
245
  const cols = [];
217
246
  for (const subjectId of subjects) {
218
247
  const details = detailMap.get(subjectId);
@@ -227,13 +256,57 @@ function buildObjectDetailColumns(config, objectDetails) {
227
256
  }
228
257
  return cols;
229
258
  }
259
+ /**
260
+ * Build `product-stage-task` columns: one column per (subject, stage, task) triple.
261
+ * Each stage in `objectDetails` that carries a `tasks[]` array is expanded to
262
+ * per-task columns `{ subjectId, stageId, taskId, label: subjectId:stageId:taskId }`.
263
+ * Stages with no tasks fall back to a single stage-grain column.
264
+ */
265
+ function buildTaskColumns(config, objectDetails) {
266
+ const detailMap = new Map(objectDetails.map(d => [d.objectId, d.details]));
267
+ const subjects = [
268
+ ...(config.subjects.products ?? []),
269
+ ...(config.subjects.processes ?? []),
270
+ ...(config.subjects.capabilities ?? []),
271
+ ];
272
+ const cols = [];
273
+ for (const subjectId of subjects) {
274
+ const stages = detailMap.get(subjectId);
275
+ if (!stages || stages.length === 0) {
276
+ cols.push({ subjectId, label: subjectId });
277
+ }
278
+ else {
279
+ for (const stage of stages) {
280
+ if (!stage.tasks || stage.tasks.length === 0) {
281
+ // Stage with no tasks: fall back to stage-grain column.
282
+ cols.push({ subjectId, stageId: stage.id, label: `${subjectId}:${stage.id}` });
283
+ }
284
+ else {
285
+ for (const task of stage.tasks) {
286
+ cols.push({
287
+ subjectId,
288
+ stageId: stage.id,
289
+ taskId: task.id,
290
+ label: `${subjectId}:${stage.id}:${task.id}`,
291
+ });
292
+ }
293
+ }
294
+ }
295
+ }
296
+ }
297
+ return cols;
298
+ }
230
299
  /**
231
300
  * Returns true when assertion `a` contributes to column `col`.
232
301
  *
233
302
  * At `'product'` grain: `a.subject` must match `col.subjectId`.
234
303
  * At `'product-stage'` grain: `a.subject` must match AND either
235
- * `a.realised_via` is absent (claim covers the whole subject, so every stage
236
- * inherits it) OR `col.stageId` ∈ `a.realised_via`.
304
+ * `a.realised_via` is absent (claim covers the whole subject) OR
305
+ * `col.stageId` ∈ `a.realised_via`.
306
+ * At `'product-stage-task'` grain: as above for stage, plus `col.taskId` ∈
307
+ * `a.realised_via` counts as a match (task-level claim covers this task column).
308
+ * A stage-level claim (`col.stageId` ∈ `a.realised_via`) covers all task columns
309
+ * in that stage.
237
310
  */
238
311
  function assertionMatchesColumn(a, col) {
239
312
  if (a.subject !== col.subjectId)
@@ -242,19 +315,30 @@ function assertionMatchesColumn(a, col) {
242
315
  return true;
243
316
  if (!a.realised_via || a.realised_via.length === 0)
244
317
  return true;
318
+ if (col.taskId) {
319
+ // Task grain: match on taskId OR on stageId (stage claim covers all tasks).
320
+ return a.realised_via.includes(col.taskId) || a.realised_via.includes(col.stageId);
321
+ }
245
322
  return a.realised_via.includes(col.stageId);
246
323
  }
247
324
  /**
248
325
  * Build the matrix per §5.2.
249
326
  *
250
327
  * At the default `grouping.columns: 'product'` grain, one column per subject.
251
- * At `grouping.columns: 'product-stage'`, pass `objectDetails` to expand each
252
- * subject into per-stage sub-columns (typically extracted from a
253
- * process-blueprint document via `extractObjectDetails`). Subjects with no
254
- * stage mapping fall back to a single product-grain column automatically.
255
328
  *
256
- * Subjects are taken from `config.subjects.products` and
257
- * `config.subjects.processes` (in that order). Subjects with no binding
329
+ * At `grouping.columns: 'product-stage'`, pass `objectDetails` (from
330
+ * `extractObjectDetails` on a process-blueprint) to expand each subject into
331
+ * per-stage sub-columns. Subjects with no stage mapping fall back to a single
332
+ * product-grain column.
333
+ *
334
+ * At `grouping.columns: 'product-stage-task'`, pass `objectDetails` where each
335
+ * stage carries a `tasks[]` array (combine blueprint stages and process-flow
336
+ * tasks via `mergeStageTaskDetails`). Each task becomes a
337
+ * `(subject, stage, task)` column. Stages with no tasks fall back to
338
+ * stage-grain columns.
339
+ *
340
+ * Subjects are taken from `config.subjects.products`, `config.subjects.processes`,
341
+ * and `config.subjects.capabilities` (in that order). Subjects with no binding
258
342
  * obligation still appear as columns so the gap is visible.
259
343
  */
260
344
  export function buildImpactMatrix(canon, config, objectDetails) {
@@ -263,11 +347,14 @@ export function buildImpactMatrix(canon, config, objectDetails) {
263
347
  assertions: canon.assertions,
264
348
  });
265
349
  const obligations = orderRows(resolveObligations(config, index, canon.requirements), config.order_rows_by);
266
- const useStageGrain = config.grouping?.columns === 'object-details' && objectDetails && objectDetails.length > 0;
267
- const columns = useStageGrain
268
- ? buildObjectDetailColumns(config, objectDetails)
269
- : buildProductColumns(config);
270
- const allowedStatuses = new Set(config.status_display?.show ?? ['compliant', 'partial', 'non_compliant', 'under_review', 'n_a']);
350
+ const grain = config.grouping?.columns ?? 'product';
351
+ const hasDetails = objectDetails !== undefined && objectDetails.length > 0;
352
+ const columns = grain === 'product-stage-task' && hasDetails
353
+ ? buildTaskColumns(config, objectDetails)
354
+ : (grain === 'product-stage' || grain === 'object-details') && hasDetails
355
+ ? buildObjectDetailColumns(config, objectDetails)
356
+ : buildProductColumns(config, [...(canon.products ?? []), ...(canon.subjects ?? [])]);
357
+ const allowedStatuses = new Set(config.status_display?.show ?? ['compliant', 'partial', 'non_compliant', 'under_review', 'pending_owner', 'n_a']);
271
358
  const rowIndex = new Set(obligations.map(r => r.id));
272
359
  const cells = obligations.map(() => columns.map(() => emptyCell()));
273
360
  for (let c = 0; c < columns.length; c++) {
@@ -357,9 +444,10 @@ function emptyCell() {
357
444
  decoration: { isNew: false, isUrgent: false, deadlineStatus: 'none' },
358
445
  };
359
446
  }
360
- // ── Process-blueprint stage extractor (CV-3a) ───────────────────────────────
447
+ // ── Process-blueprint / process-flow extractors (CV-3a) ─────────────────────
361
448
  /**
362
- * Extract `ObjectDetailInput` from a raw (YAML-parsed) process-blueprint document.
449
+ * Extract stage-level `ObjectDetailInput` from a raw (YAML-parsed)
450
+ * process-blueprint document.
363
451
  *
364
452
  * Accepts both the bare document object and the wrapped form
365
453
  * (`{ process_blueprint: { id, stages: […] } }`).
@@ -368,6 +456,8 @@ function emptyCell() {
368
456
  *
369
457
  * Pass the returned value into `buildImpactMatrix` as part of the
370
458
  * `objectDetails` array when `grouping.columns: 'product-stage'` is configured.
459
+ * For `grouping.columns: 'product-stage-task'`, combine with
460
+ * `extractProcessFlowTasks` via `mergeStageTaskDetails`.
371
461
  */
372
462
  export function extractObjectDetails(doc) {
373
463
  if (!doc || typeof doc !== 'object' || Array.isArray(doc))
@@ -396,12 +486,90 @@ export function extractObjectDetails(doc) {
396
486
  }
397
487
  return { objectId: id, details };
398
488
  }
489
+ /**
490
+ * Extract task-type steps from a raw (YAML-parsed) PROCESS element document.
491
+ *
492
+ * Reads `notation: process` documents with an inline `flow.steps[]` array.
493
+ * Filters for task-type nodes (`task`, `userTask`, `serviceTask`) — start/end
494
+ * events and gateways are excluded. Returns a flat `ObjectDetailInput` where
495
+ * `details` are the extracted task steps in declaration order.
496
+ *
497
+ * Returns `null` when the document is not a recognisable process element or
498
+ * has no `id` / `flow.steps` array.
499
+ *
500
+ * Use the returned value with `mergeStageTaskDetails` to embed tasks into
501
+ * blueprint stages for `grouping.columns: product-stage-task`.
502
+ */
503
+ export function extractProcessFlowTasks(doc) {
504
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc))
505
+ return null;
506
+ const top = doc;
507
+ if (top.notation !== 'process')
508
+ return null;
509
+ const id = typeof top.id === 'string' ? top.id : null;
510
+ if (!id)
511
+ return null;
512
+ const flow = top.flow && typeof top.flow === 'object' && !Array.isArray(top.flow)
513
+ ? top.flow
514
+ : null;
515
+ if (!flow || !Array.isArray(flow.steps))
516
+ return null;
517
+ const TASK_TYPES = new Set(['task', 'userTask', 'serviceTask', 'scriptTask',
518
+ 'receiveTask', 'sendTask', 'manualTask', 'businessRuleTask', 'callActivity', 'subProcess']);
519
+ const details = [];
520
+ for (const step of flow.steps) {
521
+ if (!step || typeof step !== 'object' || Array.isArray(step))
522
+ continue;
523
+ const s = step;
524
+ const stepId = typeof s.id === 'string' ? s.id : null;
525
+ if (!stepId)
526
+ continue;
527
+ if (!TASK_TYPES.has(s.type))
528
+ continue;
529
+ details.push({ id: stepId, name: typeof s.name === 'string' ? s.name : stepId });
530
+ }
531
+ return details.length > 0 ? { objectId: id, details } : null;
532
+ }
533
+ /**
534
+ * Combine blueprint stage details (from `extractObjectDetails`) with
535
+ * process-flow task details (from `extractProcessFlowTasks`) into a
536
+ * stage-with-tasks hierarchy for `grouping.columns: product-stage-task`.
537
+ *
538
+ * All tasks from `taskInput.details` are distributed into their parent stage
539
+ * by matching the `stageOwner` map: `stageId → taskId[]`. If `stageOwner` is
540
+ * omitted (no stage-to-task mapping is available), all tasks are placed into
541
+ * every stage (`undefined` key `→` all tasks).
542
+ *
543
+ * When `taskInput` is `null` or its `objectId` does not match `stageInput`,
544
+ * returns `stageInput` unchanged (callers can safely chain the result into
545
+ * `buildImpactMatrix` — it will degrade to stage-grain columns).
546
+ *
547
+ * @param stageInput Stage-level detail (from `extractObjectDetails`).
548
+ * @param taskInput Task-level detail (from `extractProcessFlowTasks`).
549
+ * @param stageOwner Optional mapping `stageId → taskId[]`. When provided,
550
+ * only tasks listed for a stage are embedded in it.
551
+ */
552
+ export function mergeStageTaskDetails(stageInput, taskInput, stageOwner) {
553
+ if (!taskInput || taskInput.objectId !== stageInput.objectId)
554
+ return stageInput;
555
+ const allTasks = taskInput.details;
556
+ return {
557
+ objectId: stageInput.objectId,
558
+ details: stageInput.details.map(stage => {
559
+ const stageTasks = stageOwner
560
+ ? allTasks.filter(t => (stageOwner[stage.id] ?? []).includes(t.id))
561
+ : allTasks;
562
+ return stageTasks.length > 0 ? { ...stage, tasks: stageTasks } : stage;
563
+ }),
564
+ };
565
+ }
399
566
  // ── Markdown rendering ──────────────────────────────────────────────────────
400
567
  const STATUS_GLYPH = {
401
568
  compliant: 'OK',
402
569
  partial: 'PARTIAL',
403
570
  non_compliant: 'FAIL',
404
571
  under_review: 'REVIEW',
572
+ pending_owner: 'PENDING',
405
573
  n_a: 'N/A',
406
574
  };
407
575
  function escMd(s) {
@@ -450,7 +618,7 @@ export function renderImpactMarkdown(matrix) {
450
618
  lines.push('');
451
619
  lines.push('## Legend');
452
620
  lines.push('');
453
- lines.push('- **OK / PARTIAL / FAIL / REVIEW / N/A** — aggregated `ASSERTION.status` per §5.2.');
621
+ lines.push('- **OK / PARTIAL / FAIL / REVIEW / PENDING / N/A** — aggregated `ASSERTION.status` per §5.2.');
454
622
  lines.push(`- **${escMd(matrix.emptyLabels.no_obligation_label)}** — modelling gap; no admitted ASSERTION binds this (obligation, subject) pair.`);
455
623
  lines.push(`- **${escMd(matrix.emptyLabels.no_obligation_applies_label)}** — modelled fact; an admitted ASSERTION with status \`n_a\` excludes this pair.`);
456
624
  return lines.join('\n') + '\n';
@@ -8,9 +8,9 @@ export { renderComplianceMarkdown } from './markdown.js';
8
8
  export type { ReportScope, MarkdownOptions } from './markdown.js';
9
9
  export { renderComplianceHtml, renderImpactMatrixHtml } from './html.js';
10
10
  export type { HtmlOptions, ImpactMatrixHtmlOptions } from './html.js';
11
- export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, computeDeadlineStatus } from './impact.js';
11
+ export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, extractProcessFlowTasks, mergeStageTaskDetails, computeDeadlineStatus } from './impact.js';
12
12
  export type { ImpactCell, ImpactColumn, ImpactEmptyCellLabels, ImpactGrouping, ImpactMatrix, ImpactObligationFilter, ImpactStatusDisplay, ImpactSubjects, ImpactViewConfig, ParseImpactViewConfigResult, } from './impact.js';
13
13
  export { assertionToScoringElement, complianceConfidenceHeader, requirementToScoringElement, scoreComplianceView, } from './confidence.js';
14
14
  export { parseCoverageMetricConfig, buildCoverageMatrix, } from './coverage-metric.js';
15
- export type { CoverageMetricConfig, CoverageMetricScope, CoverageMetricSubjects, CoverageMetricThresholds, CoverageMatrix, CoverageRow, ParseCoverageMetricResult, RagStatus, } from './coverage-metric.js';
15
+ export type { CoverageMetricConfig, CoverageMetricRegimes, CoverageMetricRegimesFilter, CoverageMetricSubjects, CoverageMetricThresholds, CoverageMatrix, CoverageRow, ParseCoverageMetricResult, RagStatus, } from './coverage-metric.js';
16
16
  export type { ComplianceIndex, ComplianceIndexInput, DeadlineStatus, IndexRequirement, IndexAssertion, LawTree, LawTreeRequirement, ProductView, ProductRequirementStatus, ObjectDetailDef, ObjectDetailInput, } from './types.js';
@@ -4,6 +4,6 @@ export { buildGapReport } from './gap-report.js';
4
4
  export { emptyCanon, ingestComplianceDoc } from './classify.js';
5
5
  export { renderComplianceMarkdown } from './markdown.js';
6
6
  export { renderComplianceHtml, renderImpactMatrixHtml } from './html.js';
7
- export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, computeDeadlineStatus } from './impact.js';
7
+ export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, extractProcessFlowTasks, mergeStageTaskDetails, computeDeadlineStatus } from './impact.js';
8
8
  export { assertionToScoringElement, complianceConfidenceHeader, requirementToScoringElement, scoreComplianceView, } from './confidence.js';
9
9
  export { parseCoverageMetricConfig, buildCoverageMatrix, } from './coverage-metric.js';
@@ -10,6 +10,7 @@ const STATUS_LABELS = {
10
10
  partial: 'Partial',
11
11
  non_compliant: 'Non-compliant',
12
12
  under_review: 'Under review',
13
+ pending_owner: 'Pending owner',
13
14
  n_a: 'N/A',
14
15
  };
15
16
  /** Escapes a value for a Markdown table cell (pipes + newlines). */
@@ -44,18 +44,37 @@ export interface IndexAssertion {
44
44
  * stage-grouped matrix cells.
45
45
  */
46
46
  realised_via?: string[];
47
+ /** ID or name of the party who must confirm this assertion (pending-owner state). */
48
+ owner_to_confirm?: string;
47
49
  }
48
- /** One detail-level element of a business object, used as a matrix sub-column. */
50
+ /** One stage (or task) element of a business object, used as a matrix sub-column. */
49
51
  export interface ObjectDetailDef {
50
52
  id: string;
51
53
  name: string;
54
+ /**
55
+ * Ordered task-type flow steps within this stage.
56
+ * Present only when `grouping.columns: product-stage-task` grain is active.
57
+ * Each task produces a separate matrix column: (subject, stageId, taskId).
58
+ * When absent or empty, the stage itself is the column grain.
59
+ */
60
+ tasks?: Array<{
61
+ id: string;
62
+ name: string;
63
+ }>;
52
64
  }
53
65
  /**
54
- * Maps a business object to its ordered detail elements.
55
- * Passed into `buildImpactMatrix` when `grouping.columns` is
56
- * `object-details`. The details can be process stages, product
57
- * components, capability modules, etc. — whatever sub-structure the object
58
- * exposes. Typically extracted from a process-blueprint via`extractObjectDetails`.
66
+ * Maps a business object to its ordered stages (and optionally their tasks).
67
+ * Passed into `buildImpactMatrix` to enable stage/task column grouping.
68
+ *
69
+ * - For `grouping.columns: product-stage`: `details` contains stages; each
70
+ * stage becomes one matrix column.
71
+ * - For `grouping.columns: product-stage-task`: each stage in `details` carries
72
+ * a `tasks` array; each task produces a `(subject, stage, task)` column.
73
+ * Stages with no tasks fall back to a stage-grain column.
74
+ *
75
+ * Typically built by combining `extractObjectDetails` (blueprint stages) and
76
+ * `extractProcessFlowTasks` (process flow tasks), then merging via
77
+ * `mergeStageTaskDetails`.
59
78
  */
60
79
  export interface ObjectDetailInput {
61
80
  objectId: string;
@@ -10,9 +10,13 @@ export declare const DEFAULT_EDGE_CURVATURE = 1;
10
10
  * Builds the SVG `d` for a horizontal-tangent cubic Bézier from (sx,sy) to
11
11
  * (tx,ty).
12
12
  *
13
- * `curvature` scales the control-handle length:
14
- * - 0 → control points collapse onto the endpoints ⇒ a straight line.
13
+ * `curvature` scales the exit control-handle (departure from source).
14
+ * `entryCurvature` scales the entry control-handle (arrival at target);
15
+ * defaults to `curvature` when omitted so callers that pass one value get
16
+ * symmetric handles (the historical behaviour).
17
+ *
18
+ * - 0 → handle collapses onto its endpoint ⇒ straight at that end.
15
19
  * - 1 → the historical curve (no visual change from before #76).
16
- * - >1 → progressively stronger arc.
20
+ * - >1 → progressively stronger arc at that end.
17
21
  */
18
- export declare function horizontalCubicEdgePath(sx: number, sy: number, tx: number, ty: number, curvature?: number): string;
22
+ export declare function horizontalCubicEdgePath(sx: number, sy: number, tx: number, ty: number, curvature?: number, entryCurvature?: number): string;
package/dist/edge-path.js CHANGED
@@ -21,15 +21,20 @@ export const DEFAULT_EDGE_CURVATURE = 1;
21
21
  * Builds the SVG `d` for a horizontal-tangent cubic Bézier from (sx,sy) to
22
22
  * (tx,ty).
23
23
  *
24
- * `curvature` scales the control-handle length:
25
- * - 0 → control points collapse onto the endpoints ⇒ a straight line.
24
+ * `curvature` scales the exit control-handle (departure from source).
25
+ * `entryCurvature` scales the entry control-handle (arrival at target);
26
+ * defaults to `curvature` when omitted so callers that pass one value get
27
+ * symmetric handles (the historical behaviour).
28
+ *
29
+ * - 0 → handle collapses onto its endpoint ⇒ straight at that end.
26
30
  * - 1 → the historical curve (no visual change from before #76).
27
- * - >1 → progressively stronger arc.
31
+ * - >1 → progressively stronger arc at that end.
28
32
  */
29
- export function horizontalCubicEdgePath(sx, sy, tx, ty, curvature = DEFAULT_EDGE_CURVATURE) {
33
+ export function horizontalCubicEdgePath(sx, sy, tx, ty, curvature = DEFAULT_EDGE_CURVATURE, entryCurvature) {
30
34
  const dx = tx - sx;
31
35
  const dy = ty - sy;
32
36
  const baseHandle = Math.max(EDGE_MIN_HANDLE, Math.abs(dx) * DX_FACTOR, Math.abs(dy) * DY_FACTOR);
33
- const handle = baseHandle * curvature;
34
- return `M${sx},${sy} C${sx + handle},${sy} ${tx - handle},${ty} ${tx},${ty}`;
37
+ const exitHandle = baseHandle * curvature;
38
+ const entryHandle = baseHandle * (entryCurvature ?? curvature);
39
+ return `M${sx},${sy} C${sx + exitHandle},${sy} ${tx - entryHandle},${ty} ${tx},${ty}`;
35
40
  }
@@ -1,10 +1,10 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { validateFactor } from '../validate.js';
3
- function valid() {
3
+ function validExternal() {
4
4
  return {
5
5
  notation: 'factor',
6
6
  id: 'FACTOR-EU-REG-1',
7
- name: 'EU regulatory window for market entry',
7
+ name: 'EU regulatory window',
8
8
  type: 'external',
9
9
  category: 'legal',
10
10
  description: 'Standing external driver — the EU regulatory regime.',
@@ -17,67 +17,96 @@ function valid() {
17
17
  valid_to: null,
18
18
  };
19
19
  }
20
- const codes = (input) => validateFactor(input).errors.map(e => e.code);
21
- const warns = (input) => validateFactor(input).warnings.map(w => w.code);
20
+ function validInternal() {
21
+ return {
22
+ notation: 'factor',
23
+ id: 'FACTOR-COMP-1',
24
+ name: 'Support response time',
25
+ type: 'internal',
26
+ description: 'Standing internal driver.',
27
+ zone: 'canon',
28
+ admitted_at: '2026-05-29',
29
+ admitted_by: 'v.korobeinikov',
30
+ gate_checks: { uniqueness: 'pass', consistency: 'pass', completeness: 'pass' },
31
+ valid_from: '2026-05-26',
32
+ valid_to: null,
33
+ };
34
+ }
35
+ const codes = (input, opts) => validateFactor(input, opts).errors.map(e => e.code);
22
36
  describe('validateFactor — positive', () => {
23
- it('accepts the worked-example FACTOR-EU-REG-1', () => {
24
- const r = validateFactor(valid());
37
+ it('accepts a well-formed external PESTLE factor', () => {
38
+ const r = validateFactor(validExternal());
25
39
  expect(r.valid, JSON.stringify(r.errors)).toBe(true);
26
- expect(r.errors).toHaveLength(0);
27
40
  });
28
- it('accepts a minimal factor (no type/category/references)', () => {
29
- const r = valid();
30
- delete r.type;
31
- delete r.category;
32
- delete r.references_constraint;
33
- expect(validateFactor(r).valid).toBe(true);
41
+ it('accepts a well-formed internal factor (no PESTLE category)', () => {
42
+ expect(validateFactor(validInternal()).valid).toBe(true);
34
43
  });
35
- });
36
- describe('validateFactor — FACTOR-001 (envelope / shape)', () => {
37
- it('flags a non-object', () => {
38
- expect(codes(null)).toEqual(['FACTOR-001']);
39
- expect(codes(['x'])).toEqual(['FACTOR-001']);
44
+ it('accepts a minimal factor with no optional fields', () => {
45
+ const f = validInternal();
46
+ delete f.type;
47
+ delete f.description;
48
+ expect(validateFactor(f).valid).toBe(true);
40
49
  });
41
- it('flags a wrong notation tag', () => {
42
- expect(codes({ ...valid(), notation: 'driver' })).toContain('FACTOR-001');
50
+ });
51
+ describe('validateFactor — FACTOR-001 (shape / id grammar)', () => {
52
+ it('flags a missing required envelope field', () => {
53
+ const f = validExternal();
54
+ delete f.admitted_by;
55
+ expect(codes(f)).toContain('FACTOR-001');
43
56
  });
44
- it('flags an id with the wrong TYPE prefix', () => {
45
- expect(codes({ ...valid(), id: 'GOAL-EU-1' })).toContain('FACTOR-001');
57
+ it('flags an id that violates the grammar', () => {
58
+ expect(codes({ ...validExternal(), id: 'FACTOR-EU-REG-001' })).toContain('FACTOR-001');
59
+ expect(codes({ ...validExternal(), id: 'FAC-1' })).toContain('FACTOR-001');
60
+ expect(codes({ ...validExternal(), id: 'factor-1' })).toContain('FACTOR-001');
46
61
  });
47
- it('flags an id with a leading-zero terminal', () => {
48
- expect(codes({ ...valid(), id: 'FACTOR-EU-REG-01' })).toContain('FACTOR-001');
62
+ it('flags a wrong notation tag and a non-canon zone', () => {
63
+ expect(codes({ ...validExternal(), notation: 'driver' })).toContain('FACTOR-001');
64
+ expect(codes({ ...validExternal(), zone: 'sandbox' })).toContain('FACTOR-001');
49
65
  });
50
66
  it('flags a missing valid_to key (distinct from a null value)', () => {
51
- const r = valid();
52
- delete r.valid_to;
53
- expect(codes(r)).toContain('FACTOR-001');
54
- expect(validateFactor({ ...valid(), valid_to: null }).valid).toBe(true);
67
+ const f = validExternal();
68
+ delete f.valid_to;
69
+ expect(codes(f)).toContain('FACTOR-001');
55
70
  });
56
- it('flags references_constraint entries that are not CONSTRAINT typed ids', () => {
57
- expect(codes({ ...valid(), references_constraint: ['GOAL-EU-1'] })).toContain('FACTOR-001');
58
- expect(codes({ ...valid(), references_constraint: 'CONSTRAINT-X-1' })).toContain('FACTOR-001');
71
+ it('rejects a non-object', () => {
72
+ expect(codes(null)).toEqual(['FACTOR-001']);
73
+ expect(codes(['a'])).toEqual(['FACTOR-001']);
59
74
  });
60
75
  });
61
76
  describe('validateFactor — FACTOR-002 (type enum)', () => {
62
- it('flags a type outside external/internal', () => {
63
- expect(codes({ ...valid(), type: 'driver' })).toContain('FACTOR-002');
64
- });
65
- it('accepts each permitted type', () => {
66
- for (const t of ['external', 'internal']) {
67
- expect(codes({ ...valid(), type: t, category: undefined })).not.toContain('FACTOR-002');
68
- }
77
+ it('flags a type value outside {external, internal}', () => {
78
+ expect(codes({ ...validExternal(), type: 'driver' })).toContain('FACTOR-002');
69
79
  });
70
80
  });
71
81
  describe('validateFactor — FACTOR-003 (PESTLE category)', () => {
72
- it('flags a category outside the PESTLE classes', () => {
73
- expect(codes({ ...valid(), category: 'cultural' })).toContain('FACTOR-003');
82
+ it('flags a category outside PESTLE', () => {
83
+ expect(codes({ ...validExternal(), category: 'cosmic' })).toContain('FACTOR-003');
74
84
  });
75
- it('warns when category is set on an internal factor', () => {
76
- expect(warns({ ...valid(), type: 'internal', category: 'economic' })).toContain('FACTOR-003');
85
+ it('flags PESTLE category on an internal factor', () => {
86
+ const f = validInternal();
87
+ f.category = 'legal';
88
+ expect(codes(f)).toContain('FACTOR-003');
77
89
  });
78
- it('accepts each permitted PESTLE class', () => {
79
- for (const c of ['political', 'economic', 'social', 'technological', 'legal', 'environmental']) {
80
- expect(codes({ ...valid(), category: c })).not.toContain('FACTOR-003');
90
+ it('accepts each permitted PESTLE category on an external factor', () => {
91
+ for (const cat of ['political', 'economic', 'social', 'technological', 'legal', 'environmental']) {
92
+ const f = validExternal();
93
+ f.category = cat;
94
+ expect(codes(f)).not.toContain('FACTOR-003');
81
95
  }
82
96
  });
83
97
  });
98
+ describe('validateFactor — FACTOR-004 (references_constraint)', () => {
99
+ it('flags a malformed references_constraint entry', () => {
100
+ expect(codes({ ...validExternal(), references_constraint: ['not an id'] })).toContain('FACTOR-004');
101
+ });
102
+ it('flags a references_constraint entry of the wrong TYPE', () => {
103
+ expect(codes({ ...validExternal(), references_constraint: ['GOAL-X-1'] })).toContain('FACTOR-004');
104
+ });
105
+ it('flags an unresolved id when a catalog is supplied', () => {
106
+ const catalog = { typeOf: () => undefined };
107
+ expect(codes(validExternal(), { catalog })).toContain('FACTOR-004');
108
+ });
109
+ it('does not flag without a catalog for a well-formed CONSTRAINT id', () => {
110
+ expect(codes(validExternal())).not.toContain('FACTOR-004');
111
+ });
112
+ });
@@ -1,3 +1,4 @@
1
- export type { Factor as FactorPrimitive, FactorType, FactorCategory } from './types.js';
2
- export { FACTOR_TYPES, FACTOR_CATEGORIES } from './types.js';
1
+ export type { Factor, FactorType, FactorPestleCategory } from './types.js';
2
+ export { FACTOR_PESTLE_CATEGORIES } from './types.js';
3
3
  export { validateFactor } from './validate.js';
4
+ export type { FactorValidateOptions } from './validate.js';
@@ -1,2 +1,2 @@
1
- export { FACTOR_TYPES, FACTOR_CATEGORIES } from './types.js';
1
+ export { FACTOR_PESTLE_CATEGORIES } from './types.js';
2
2
  export { validateFactor } from './validate.js';