@forwardimpact/schema 0.3.0 → 0.6.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 (46) hide show
  1. package/bin/fit-schema.js +2 -2
  2. package/examples/capabilities/business.yaml +1 -1
  3. package/examples/capabilities/delivery.yaml +9 -7
  4. package/examples/capabilities/people.yaml +1 -1
  5. package/examples/capabilities/reliability.yaml +32 -11
  6. package/examples/capabilities/scale.yaml +1 -1
  7. package/examples/framework.yaml +1 -1
  8. package/examples/questions/behaviours/outcome_ownership.yaml +226 -49
  9. package/examples/questions/behaviours/polymathic_knowledge.yaml +273 -45
  10. package/examples/questions/behaviours/precise_communication.yaml +246 -52
  11. package/examples/questions/behaviours/relentless_curiosity.yaml +246 -48
  12. package/examples/questions/behaviours/systems_thinking.yaml +236 -50
  13. package/examples/questions/capabilities/business.yaml +107 -0
  14. package/examples/questions/capabilities/delivery.yaml +104 -0
  15. package/examples/questions/capabilities/people.yaml +104 -0
  16. package/examples/questions/capabilities/reliability.yaml +103 -0
  17. package/examples/questions/capabilities/scale.yaml +103 -0
  18. package/examples/questions/skills/architecture_design.yaml +102 -51
  19. package/examples/questions/skills/cloud_platforms.yaml +90 -44
  20. package/examples/questions/skills/code_quality.yaml +86 -45
  21. package/examples/questions/skills/data_modeling.yaml +93 -43
  22. package/examples/questions/skills/devops.yaml +91 -44
  23. package/examples/questions/skills/full_stack_development.yaml +93 -45
  24. package/examples/questions/skills/sre_practices.yaml +92 -41
  25. package/examples/questions/skills/stakeholder_management.yaml +97 -46
  26. package/examples/questions/skills/team_collaboration.yaml +87 -40
  27. package/examples/questions/skills/technical_writing.yaml +89 -40
  28. package/examples/stages.yaml +6 -0
  29. package/package.json +9 -9
  30. package/schema/json/behaviour-questions.schema.json +53 -26
  31. package/schema/json/capability-questions.schema.json +95 -0
  32. package/schema/json/capability.schema.json +3 -3
  33. package/schema/json/skill-questions.schema.json +34 -19
  34. package/schema/json/stages.schema.json +5 -1
  35. package/schema/rdf/behaviour-questions.ttl +39 -7
  36. package/schema/rdf/capability.ttl +5 -5
  37. package/schema/rdf/defs.ttl +3 -3
  38. package/schema/rdf/skill-questions.ttl +28 -1
  39. package/schema/rdf/stages.ttl +27 -3
  40. package/{lib → src}/levels.js +37 -80
  41. package/{lib → src}/loader.js +9 -5
  42. package/{lib → src}/modifiers.js +3 -3
  43. package/{lib → src}/validation.js +74 -37
  44. /package/{lib → src}/index-generator.js +0 -0
  45. /package/{lib → src}/index.js +0 -0
  46. /package/{lib → src}/schema-validation.js +0 -0
@@ -7,7 +7,8 @@
7
7
  # =============================================================================
8
8
  # Skill Questions Schema
9
9
  # =============================================================================
10
- # Interview questions for assessing skills.
10
+ # Interview questions for assessing skills, organized by role type
11
+ # (professional/management) and level.
11
12
  # =============================================================================
12
13
 
13
14
  # -----------------------------------------------------------------------------
@@ -18,6 +19,18 @@ fit:Question a rdfs:Class ;
18
19
  rdfs:label "Question"@en ;
19
20
  rdfs:comment "Interview question for assessing skills or behaviours"@en .
20
21
 
22
+ fit:RoleType a rdfs:Class ;
23
+ rdfs:label "RoleType"@en ;
24
+ rdfs:comment "Type of role: professional (IC) or management"@en .
25
+
26
+ fit:professional a fit:RoleType ;
27
+ rdfs:label "professional"@en ;
28
+ rdfs:comment "Professional/individual contributor roles"@en .
29
+
30
+ fit:management a fit:RoleType ;
31
+ rdfs:label "management"@en ;
32
+ rdfs:comment "Management roles"@en .
33
+
21
34
  # -----------------------------------------------------------------------------
22
35
  # Properties
23
36
  # -----------------------------------------------------------------------------
@@ -58,6 +71,12 @@ fit:atLevel a rdf:Property ;
58
71
  rdfs:domain fit:Question ;
59
72
  rdfs:range fit:SkillLevel .
60
73
 
74
+ fit:forRoleType a rdf:Property ;
75
+ rdfs:label "forRoleType"@en ;
76
+ rdfs:comment "The role type this question is intended for (professional or management)"@en ;
77
+ rdfs:domain fit:Question ;
78
+ rdfs:range fit:RoleType .
79
+
61
80
  # =============================================================================
62
81
  # SHACL SHAPES
63
82
  # =============================================================================
@@ -125,4 +144,12 @@ fit:SkillQuestionShape a sh:NodeShape ;
125
144
  sh:maxCount 1 ;
126
145
  sh:name "atLevel" ;
127
146
  sh:description "The skill level this question is for" ;
147
+ ] ;
148
+ sh:property [
149
+ sh:path fit:forRoleType ;
150
+ sh:in ( fit:professional fit:management ) ;
151
+ sh:minCount 1 ;
152
+ sh:maxCount 1 ;
153
+ sh:name "forRoleType" ;
154
+ sh:description "The role type this question is for" ;
128
155
  ] .
@@ -33,6 +33,17 @@ fit:entryCriteria a rdf:Property ;
33
33
  rdfs:comment "Conditions that must be met before entering this stage"@en ;
34
34
  rdfs:range xsd:string .
35
35
 
36
+ fit:exitCriteria a rdf:Property ;
37
+ rdfs:label "exitCriteria"@en ;
38
+ rdfs:comment "Conditions that must be met before leaving this stage"@en ;
39
+ rdfs:range xsd:string .
40
+
41
+ fit:summary a rdf:Property ;
42
+ rdfs:label "summary"@en ;
43
+ rdfs:comment "Third-person summary for metadata, listings, and sub-agent descriptions"@en ;
44
+ rdfs:domain fit:Stage ;
45
+ rdfs:range xsd:string .
46
+
36
47
  fit:targetStage a rdf:Property ;
37
48
  rdfs:label "targetStage"@en ;
38
49
  rdfs:comment "The stage to transition to"@en ;
@@ -63,7 +74,7 @@ fit:StageShape a sh:NodeShape ;
63
74
  sh:targetClass fit:Stage ;
64
75
  sh:property [
65
76
  sh:path fit:id ;
66
- sh:in ( "plan" "code" "review" ) ;
77
+ sh:in ( "specify" "plan" "code" "review" "deploy" ) ;
67
78
  sh:minCount 1 ;
68
79
  sh:maxCount 1 ;
69
80
  sh:name "id" ;
@@ -89,7 +100,14 @@ fit:StageShape a sh:NodeShape ;
89
100
  sh:datatype xsd:string ;
90
101
  sh:maxCount 1 ;
91
102
  sh:name "description" ;
92
- sh:description "Description of the stage's purpose" ;
103
+ sh:description "Description of the stage's purpose (second-person, for agent body)" ;
104
+ ] ;
105
+ sh:property [
106
+ sh:path fit:summary ;
107
+ sh:datatype xsd:string ;
108
+ sh:maxCount 1 ;
109
+ sh:name "summary" ;
110
+ sh:description "Third-person summary for metadata, listings, and sub-agent descriptions" ;
93
111
  ] ;
94
112
  sh:property [
95
113
  sh:path fit:handoffs ;
@@ -108,6 +126,12 @@ fit:StageShape a sh:NodeShape ;
108
126
  sh:datatype xsd:string ;
109
127
  sh:name "entryCriteria" ;
110
128
  sh:description "Conditions that must be met before entering this stage" ;
129
+ ] ;
130
+ sh:property [
131
+ sh:path fit:exitCriteria ;
132
+ sh:datatype xsd:string ;
133
+ sh:name "exitCriteria" ;
134
+ sh:description "Conditions that must be met before leaving this stage" ;
111
135
  ] .
112
136
 
113
137
  # -----------------------------------------------------------------------------
@@ -118,7 +142,7 @@ fit:HandoffShape a sh:NodeShape ;
118
142
  sh:targetClass fit:Handoff ;
119
143
  sh:property [
120
144
  sh:path fit:targetStage ;
121
- sh:in ( "plan" "code" "review" ) ;
145
+ sh:in ( "specify" "plan" "code" "review" "deploy" ) ;
122
146
  sh:minCount 1 ;
123
147
  sh:maxCount 1 ;
124
148
  sh:name "targetStage" ;
@@ -98,72 +98,44 @@ export const Capability = {
98
98
  PRODUCT: "product",
99
99
  };
100
100
 
101
- /**
102
- * Ordered array of capabilities for consistent display
103
- * Groups related capabilities logically:
104
- * 1. Core delivery
105
- * 2. Data & AI capabilities
106
- * 3. Scale & reliability
107
- * 4. People & process
108
- * 5. Business, documentation & product
109
- * @type {string[]}
110
- */
111
- export const CAPABILITY_ORDER = [
112
- Capability.DELIVERY,
113
- Capability.DATA,
114
- Capability.AI,
115
- Capability.ML,
116
- Capability.SCALE,
117
- Capability.RELIABILITY,
118
- Capability.PEOPLE,
119
- Capability.PROCESS,
120
- Capability.BUSINESS,
121
- Capability.DOCUMENTATION,
122
- Capability.PRODUCT,
123
- ];
124
-
125
- /**
126
- * Get the index of a capability in the ordered list
127
- * @param {string} capability - The capability to look up
128
- * @returns {number} The index (0-based), or -1 if not found
129
- */
130
- export function getCapabilityIndex(capability) {
131
- return CAPABILITY_ORDER.indexOf(capability);
132
- }
101
+ // ============================================================================
102
+ // Data-driven Capability Functions
103
+ // ============================================================================
104
+ // These functions work with loaded capability data for responsibility derivation
133
105
 
134
106
  /**
135
- * Compare two capabilities for sorting
136
- * @param {string} a - First capability
137
- * @param {string} b - Second capability
138
- * @returns {number} Comparison result for sorting
107
+ * Get capability metadata from loaded capability data
108
+ * @param {Object[]} capabilities - Loaded capabilities array
109
+ * @param {string} capabilityId - The capability ID to look up
110
+ * @returns {Object|undefined} The capability object or undefined
139
111
  */
140
- export function compareCapabilities(a, b) {
141
- return getCapabilityIndex(a) - getCapabilityIndex(b);
112
+ export function getCapabilityById(capabilities, capabilityId) {
113
+ return capabilities.find((c) => c.id === capabilityId);
142
114
  }
143
115
 
144
116
  /**
145
- * Sort an array of skills by capability order, then by name
146
- * @param {import('./levels.js').Skill[]} skills - Array of skills to sort
147
- * @returns {import('./levels.js').Skill[]} Sorted array (new array, does not mutate input)
117
+ * Get ordered capability IDs from loaded capability data
118
+ * @param {Object[]} capabilities - Loaded capabilities array
119
+ * @returns {string[]} Capability IDs in display order
148
120
  */
149
- export function sortSkillsByCapability(skills) {
150
- return [...skills].sort((a, b) => {
151
- const capabilityCompare = compareCapabilities(a.capability, b.capability);
152
- if (capabilityCompare !== 0) return capabilityCompare;
153
- return a.name.localeCompare(b.name);
154
- });
121
+ export function getCapabilityOrder(capabilities) {
122
+ return [...capabilities]
123
+ .sort((a, b) => (a.ordinalRank || 0) - (b.ordinalRank || 0))
124
+ .map((c) => c.id);
155
125
  }
156
126
 
157
127
  /**
158
- * Group skills by capability in the defined order
128
+ * Group skills by capability in display order
159
129
  * @param {import('./levels.js').Skill[]} skills - Array of skills to group
160
- * @returns {Object<string, import('./levels.js').Skill[]>} Object with capabilities as keys (in order)
130
+ * @param {Object[]} capabilities - Loaded capabilities array for ordering
131
+ * @returns {Object<string, import('./levels.js').Skill[]>} Object with capabilities as keys (in display order)
161
132
  */
162
- export function groupSkillsByCapability(skills) {
133
+ export function groupSkillsByCapability(skills, capabilities) {
134
+ const capabilityOrder = getCapabilityOrder(capabilities);
163
135
  const result = {};
164
136
 
165
- // Initialize all capabilities in order (ensures consistent key order)
166
- for (const capability of CAPABILITY_ORDER) {
137
+ // Initialize all capabilities in display order (ensures consistent key order)
138
+ for (const capability of capabilityOrder) {
167
139
  result[capability] = [];
168
140
  }
169
141
 
@@ -186,32 +158,6 @@ export function groupSkillsByCapability(skills) {
186
158
  return result;
187
159
  }
188
160
 
189
- // ============================================================================
190
- // Data-driven Capability Functions
191
- // ============================================================================
192
- // These functions work with loaded capability data for responsibility derivation
193
-
194
- /**
195
- * Get capability metadata from loaded capability data
196
- * @param {Object[]} capabilities - Loaded capabilities array
197
- * @param {string} capabilityId - The capability ID to look up
198
- * @returns {Object|undefined} The capability object or undefined
199
- */
200
- export function getCapabilityById(capabilities, capabilityId) {
201
- return capabilities.find((c) => c.id === capabilityId);
202
- }
203
-
204
- /**
205
- * Get ordered capability IDs from loaded capability data
206
- * @param {Object[]} capabilities - Loaded capabilities array
207
- * @returns {string[]} Capability IDs in display order
208
- */
209
- export function getCapabilityOrder(capabilities) {
210
- return [...capabilities]
211
- .sort((a, b) => (a.displayOrder || 0) - (b.displayOrder || 0))
212
- .map((c) => c.id);
213
- }
214
-
215
161
  /**
216
162
  * Get emoji for a capability from loaded capability data
217
163
  * @param {Object[]} capabilities - Loaded capabilities array
@@ -400,10 +346,21 @@ export const SkillType = {
400
346
  * @property {number} [expectedDurationMinutes] - Estimated time to ask and answer
401
347
  */
402
348
 
349
+ /**
350
+ * @typedef {Object<string, Question[]>} LevelQuestions - Questions organized by level
351
+ */
352
+
353
+ /**
354
+ * @typedef {Object} RoleTypeQuestions
355
+ * @property {LevelQuestions} [professionalQuestions] - Questions for professional/IC roles
356
+ * @property {LevelQuestions} [managementQuestions] - Questions for management roles
357
+ */
358
+
403
359
  /**
404
360
  * @typedef {Object} QuestionBank
405
- * @property {Object<string, Object<string, Question[]>>} skillLevels - Questions by skill ID, then by level
406
- * @property {Object<string, Object<string, Question[]>>} behaviourMaturities - Questions by behaviour ID, then by maturity
361
+ * @property {Object<string, RoleTypeQuestions>} skillLevels - Questions by skill ID, then by role type (professional/management), then by level
362
+ * @property {Object<string, RoleTypeQuestions>} behaviourMaturities - Questions by behaviour ID, then by role type, then by maturity
363
+ * @property {Object<string, RoleTypeQuestions>} [capabilityLevels] - Questions by capability ID, then by role type, then by level
407
364
  */
408
365
 
409
366
  /**
@@ -281,12 +281,16 @@ async function loadCapabilitiesFromDir(capabilitiesDir) {
281
281
  * @returns {Promise<import('./levels.js').QuestionBank>}
282
282
  */
283
283
  export async function loadQuestionFolder(questionsDir) {
284
- const [skillLevels, behaviourMaturities] = await Promise.all([
285
- loadQuestionsFromDir(join(questionsDir, "skills")),
286
- loadQuestionsFromDir(join(questionsDir, "behaviours")),
287
- ]);
284
+ const [skillLevels, behaviourMaturities, capabilityLevels] =
285
+ await Promise.all([
286
+ loadQuestionsFromDir(join(questionsDir, "skills")),
287
+ loadQuestionsFromDir(join(questionsDir, "behaviours")),
288
+ loadQuestionsFromDir(join(questionsDir, "capabilities")).catch(
289
+ () => ({}),
290
+ ),
291
+ ]);
288
292
 
289
- return { skillLevels, behaviourMaturities };
293
+ return { skillLevels, behaviourMaturities, capabilityLevels };
290
294
  }
291
295
 
292
296
  /**
@@ -5,13 +5,13 @@
5
5
  * Full modifier logic is in @forwardimpact/model.
6
6
  */
7
7
 
8
- import { CAPABILITY_ORDER } from "./levels.js";
8
+ import { Capability } from "./levels.js";
9
9
 
10
10
  /**
11
- * Valid skill capability names
11
+ * Valid skill capability names (derived from Capability enum)
12
12
  * @type {Set<string>}
13
13
  */
14
- const VALID_CAPABILITIES = new Set(CAPABILITY_ORDER);
14
+ const VALID_CAPABILITIES = new Set(Object.values(Capability));
15
15
 
16
16
  /**
17
17
  * Check if a key is a skill capability
@@ -1774,6 +1774,7 @@ export function validateQuestionBank(questionBank, skills, behaviours) {
1774
1774
  const warnings = [];
1775
1775
  const skillIds = new Set(skills.map((s) => s.id));
1776
1776
  const behaviourIds = new Set(behaviours.map((b) => b.id));
1777
+ const validRoleTypes = ["professionalQuestions", "managementQuestions"];
1777
1778
 
1778
1779
  if (!questionBank) {
1779
1780
  return createValidationResult(false, [
@@ -1784,7 +1785,7 @@ export function validateQuestionBank(questionBank, skills, behaviours) {
1784
1785
  // Validate skill questions
1785
1786
  if (questionBank.skillLevels) {
1786
1787
  Object.entries(questionBank.skillLevels).forEach(
1787
- ([skillId, levelQuestions]) => {
1788
+ ([skillId, roleTypeQuestions]) => {
1788
1789
  if (!skillIds.has(skillId)) {
1789
1790
  errors.push(
1790
1791
  createError(
@@ -1795,27 +1796,46 @@ export function validateQuestionBank(questionBank, skills, behaviours) {
1795
1796
  ),
1796
1797
  );
1797
1798
  }
1798
- Object.entries(levelQuestions || {}).forEach(([level, questions]) => {
1799
- if (getSkillLevelIndex(level) === -1) {
1800
- errors.push(
1801
- createError(
1802
- "INVALID_VALUE",
1803
- `Question bank has invalid skill level: ${level}`,
1804
- `questionBank.skillLevels.${skillId}.${level}`,
1805
- level,
1806
- ),
1807
- );
1808
- }
1809
- if (!Array.isArray(questions) || questions.length === 0) {
1810
- warnings.push(
1811
- createWarning(
1812
- "EMPTY_QUESTIONS",
1813
- `No questions for skill ${skillId} at level ${level}`,
1814
- `questionBank.skillLevels.${skillId}.${level}`,
1815
- ),
1799
+ // Validate each role type (professional/management)
1800
+ Object.entries(roleTypeQuestions || {}).forEach(
1801
+ ([roleType, levelQuestions]) => {
1802
+ if (!validRoleTypes.includes(roleType)) {
1803
+ errors.push(
1804
+ createError(
1805
+ "INVALID_VALUE",
1806
+ `Question bank has invalid role type: ${roleType}`,
1807
+ `questionBank.skillLevels.${skillId}.${roleType}`,
1808
+ roleType,
1809
+ ),
1810
+ );
1811
+ return;
1812
+ }
1813
+ // Validate each level within the role type
1814
+ Object.entries(levelQuestions || {}).forEach(
1815
+ ([level, questions]) => {
1816
+ if (getSkillLevelIndex(level) === -1) {
1817
+ errors.push(
1818
+ createError(
1819
+ "INVALID_VALUE",
1820
+ `Question bank has invalid skill level: ${level}`,
1821
+ `questionBank.skillLevels.${skillId}.${roleType}.${level}`,
1822
+ level,
1823
+ ),
1824
+ );
1825
+ }
1826
+ if (!Array.isArray(questions) || questions.length === 0) {
1827
+ warnings.push(
1828
+ createWarning(
1829
+ "EMPTY_QUESTIONS",
1830
+ `No questions for skill ${skillId} (${roleType}) at level ${level}`,
1831
+ `questionBank.skillLevels.${skillId}.${roleType}.${level}`,
1832
+ ),
1833
+ );
1834
+ }
1835
+ },
1816
1836
  );
1817
- }
1818
- });
1837
+ },
1838
+ );
1819
1839
  },
1820
1840
  );
1821
1841
  }
@@ -1823,7 +1843,7 @@ export function validateQuestionBank(questionBank, skills, behaviours) {
1823
1843
  // Validate behaviour questions
1824
1844
  if (questionBank.behaviourMaturities) {
1825
1845
  Object.entries(questionBank.behaviourMaturities).forEach(
1826
- ([behaviourId, maturityQuestions]) => {
1846
+ ([behaviourId, roleTypeQuestions]) => {
1827
1847
  if (!behaviourIds.has(behaviourId)) {
1828
1848
  errors.push(
1829
1849
  createError(
@@ -1834,27 +1854,44 @@ export function validateQuestionBank(questionBank, skills, behaviours) {
1834
1854
  ),
1835
1855
  );
1836
1856
  }
1837
- Object.entries(maturityQuestions || {}).forEach(
1838
- ([maturity, questions]) => {
1839
- if (getBehaviourMaturityIndex(maturity) === -1) {
1857
+ // Validate each role type (professional/management)
1858
+ Object.entries(roleTypeQuestions || {}).forEach(
1859
+ ([roleType, maturityQuestions]) => {
1860
+ if (!validRoleTypes.includes(roleType)) {
1840
1861
  errors.push(
1841
1862
  createError(
1842
1863
  "INVALID_VALUE",
1843
- `Question bank has invalid behaviour maturity: ${maturity}`,
1844
- `questionBank.behaviourMaturities.${behaviourId}.${maturity}`,
1845
- maturity,
1846
- ),
1847
- );
1848
- }
1849
- if (!Array.isArray(questions) || questions.length === 0) {
1850
- warnings.push(
1851
- createWarning(
1852
- "EMPTY_QUESTIONS",
1853
- `No questions for behaviour ${behaviourId} at maturity ${maturity}`,
1854
- `questionBank.behaviourMaturities.${behaviourId}.${maturity}`,
1864
+ `Question bank has invalid role type: ${roleType}`,
1865
+ `questionBank.behaviourMaturities.${behaviourId}.${roleType}`,
1866
+ roleType,
1855
1867
  ),
1856
1868
  );
1869
+ return;
1857
1870
  }
1871
+ // Validate each maturity level within the role type
1872
+ Object.entries(maturityQuestions || {}).forEach(
1873
+ ([maturity, questions]) => {
1874
+ if (getBehaviourMaturityIndex(maturity) === -1) {
1875
+ errors.push(
1876
+ createError(
1877
+ "INVALID_VALUE",
1878
+ `Question bank has invalid behaviour maturity: ${maturity}`,
1879
+ `questionBank.behaviourMaturities.${behaviourId}.${roleType}.${maturity}`,
1880
+ maturity,
1881
+ ),
1882
+ );
1883
+ }
1884
+ if (!Array.isArray(questions) || questions.length === 0) {
1885
+ warnings.push(
1886
+ createWarning(
1887
+ "EMPTY_QUESTIONS",
1888
+ `No questions for behaviour ${behaviourId} (${roleType}) at maturity ${maturity}`,
1889
+ `questionBank.behaviourMaturities.${behaviourId}.${roleType}.${maturity}`,
1890
+ ),
1891
+ );
1892
+ }
1893
+ },
1894
+ );
1858
1895
  },
1859
1896
  );
1860
1897
  },
File without changes
File without changes
File without changes