@aikdna/studio-core 0.6.0 → 0.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aikdna/studio-core",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "KDNA Studio Core — pure logic library for authoring, validating, and compiling KDNA domain judgment packages.",
5
5
  "type": "commonjs",
6
6
  "main": "src/index.js",
@@ -172,6 +172,14 @@ function compileDomain(project) {
172
172
  if (evolution) files['KDNA_Evolution.json'] = JSON.stringify(evolution, null, 2);
173
173
  files['kdna.json'] = JSON.stringify(compileManifest(project, files), null, 2);
174
174
 
175
+ // ── KDNA Card (governance metadata) ─────────────────────────────
176
+ if (project.governance) {
177
+ const { generateKdnaCard } = require('../governance');
178
+ const prov = require('../provenance').buildProvenance(project, files);
179
+ const kdnaCard = generateKdnaCard(project, {}, prov);
180
+ files['KDNA_CARD.json'] = JSON.stringify(kdnaCard, null, 2);
181
+ }
182
+
175
183
  const excludedCount = cards.filter(c => !c.locked && !['deprecated'].includes(c.status)).length;
176
184
 
177
185
  return {
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Governance risk assessment — classify domain risk level and validate governance metadata.
3
+ *
4
+ * Risk levels:
5
+ * R0 — Low: inconvenience, not harm
6
+ * R1 — Medium: suboptimal outcomes
7
+ * R2 — High: significant harm possible
8
+ * R3 — Restricted: serious harm, not for public registry
9
+ */
10
+
11
+ const HIGH_RISK_KEYWORDS = {
12
+ medical: ['diagnosis', 'treatment', 'symptom', 'patient', 'clinical', 'therapy', 'medication', 'disease', 'prescription', 'surgery', 'medical'],
13
+ legal: ['lawsuit', 'liability', 'plaintiff', 'defendant', 'jurisdiction', 'statute', 'legal advice', 'attorney', 'court', 'litigation'],
14
+ financial: ['investment', 'portfolio', 'stock', 'bond', 'retirement', 'insurance', 'mortgage', 'loan', 'tax advice', 'credit score', 'financial advice'],
15
+ safety: ['weapon', 'surveillance', 'monitoring', 'tracking', 'child safety', 'emergency response', 'public safety', 'self-harm', 'suicide'],
16
+ decision: ['hiring', 'firing', 'termination', 'employment decision', 'performance review'],
17
+ };
18
+
19
+ function computeRiskLevel(project) {
20
+ const cards = project.cards || [];
21
+ const allText = cards.map(c => {
22
+ const fields = c.fields || {};
23
+ return [fields.one_sentence, fields.full_statement, fields.wrong, fields.correct, fields.question,
24
+ fields.essence, fields.scope, fields.out_of_scope,
25
+ ...(fields.applies_when || []), ...(fields.does_not_apply_when || [])]
26
+ .filter(Boolean).join(' ').toLowerCase();
27
+ }).join(' ');
28
+
29
+ // Check declared risk level first
30
+ const declared = (project.governance && project.governance.risk_level) || null;
31
+ if (declared === 'R3') return 'R3';
32
+
33
+ // Check for high-risk keywords
34
+ let detectedCategory = null;
35
+ for (const [category, keywords] of Object.entries(HIGH_RISK_KEYWORDS)) {
36
+ for (const kw of keywords) {
37
+ if (allText.includes(kw)) { detectedCategory = category; break; }
38
+ }
39
+ if (detectedCategory) break;
40
+ }
41
+
42
+ // If no high-risk keywords found and R0-R2 declared, trust the declaration
43
+ if (!detectedCategory && declared) return declared;
44
+
45
+ // Default risk levels
46
+ if (['medical', 'safety'].includes(detectedCategory)) return 'R3';
47
+ if (['legal', 'financial'].includes(detectedCategory)) return 'R2';
48
+ if (detectedCategory === 'decision') return 'R1';
49
+
50
+ return declared || 'R1'; // Default: medium risk
51
+ }
52
+
53
+ function requiresExpertReview(riskLevel) {
54
+ return riskLevel === 'R2' || riskLevel === 'R3';
55
+ }
56
+
57
+ function validateGovernance(project) {
58
+ const issues = [];
59
+ const gov = project.governance || {};
60
+
61
+ // Required fields
62
+ if (!gov.risk_level) {
63
+ issues.push({ type: 'missing_risk_level', severity: 'blocking', message: 'Governance: risk_level must be declared (R0/R1/R2/R3)' });
64
+ }
65
+ if (!gov.intended_use || !gov.intended_use.length) {
66
+ issues.push({ type: 'missing_intended_use', severity: 'blocking', message: 'Governance: intended_use must be declared' });
67
+ }
68
+ if (!gov.out_of_scope || !gov.out_of_scope.length) {
69
+ issues.push({ type: 'missing_out_of_scope', severity: 'blocking', message: 'Governance: out_of_scope must be declared' });
70
+ }
71
+ if (!gov.known_limitations || !gov.known_limitations.length) {
72
+ issues.push({ type: 'missing_limitations', severity: 'blocking', message: 'Governance: known_limitations must be declared' });
73
+ }
74
+
75
+ // Risk level specific checks
76
+ const riskLevel = gov.risk_level || computeRiskLevel(project);
77
+ if (requiresExpertReview(riskLevel)) {
78
+ if (!gov.reviewed_by) {
79
+ issues.push({ type: 'requires_expert_review', severity: 'blocking', message: `Governance: risk_level ${riskLevel} requires expert_review. reviewed_by must be set.` });
80
+ }
81
+ if (!gov.risk_warnings || !gov.risk_warnings.length) {
82
+ issues.push({ type: 'missing_risk_warnings', severity: 'blocking', message: `Governance: risk_level ${riskLevel} requires risk_warnings.` });
83
+ }
84
+ }
85
+
86
+ // Check for high-risk keywords in content that might not match declared level
87
+ const detectedLevel = computeRiskLevel(project);
88
+ if (gov.risk_level && ['R0', 'R1'].includes(gov.risk_level) && ['R2', 'R3'].includes(detectedLevel)) {
89
+ issues.push({
90
+ type: 'risk_mismatch',
91
+ severity: 'blocking',
92
+ message: `Governance: declared risk_level ${gov.risk_level} but content analysis suggests ${detectedLevel}. Review required.`,
93
+ });
94
+ }
95
+
96
+ // Author responsibility required for R1+
97
+ if (['R1', 'R2', 'R3'].includes(riskLevel) && !gov.author_statement) {
98
+ issues.push({ type: 'missing_author_statement', severity: 'blocking', message: `Governance: risk_level ${riskLevel} requires author_statement.` });
99
+ }
100
+
101
+ return {
102
+ valid: issues.filter(i => i.severity === 'blocking').length === 0,
103
+ issues,
104
+ risk_level: riskLevel,
105
+ requires_expert_review: requiresExpertReview(riskLevel),
106
+ };
107
+ }
108
+
109
+ function generateKdnaCard(project, compiledStats, provenance) {
110
+ const gov = project.governance || {};
111
+ const cards = project.cards || [];
112
+ const lockedCards = cards.filter(c => c.locked);
113
+
114
+ return {
115
+ name: project.name,
116
+ version: (project.release && project.release.version) || '0.1.0',
117
+ risk_level: gov.risk_level || computeRiskLevel(project),
118
+ intended_use: gov.intended_use || [],
119
+ out_of_scope: gov.out_of_scope || [],
120
+ known_limitations: gov.known_limitations || [],
121
+ author_responsibility: gov.author_statement || '',
122
+ risk_warnings: gov.risk_warnings || [],
123
+ human_lock_summary: {
124
+ locked_cards: lockedCards.length,
125
+ locked_axioms: lockedCards.filter(c => c.type === 'axiom').length,
126
+ locked_misunderstandings: lockedCards.filter(c => c.type === 'misunderstanding').length,
127
+ locked_self_checks: lockedCards.filter(c => c.type === 'self_check').length,
128
+ feynman_restatements: lockedCards.filter(c => c.feynman_restatement).length,
129
+ locked_by: (project.author && project.author.id) || 'unknown',
130
+ },
131
+ quality_badge: (compiledStats && compiledStats.locked_cards > 0) ? 'tested' : 'untested',
132
+ review_status: gov.review_status || 'community',
133
+ requires_expert_review: requiresExpertReview(gov.risk_level || 'R1'),
134
+ provenance: provenance || {},
135
+ license: (project.release && project.release.license) || 'CC-BY-4.0',
136
+ };
137
+ }
138
+
139
+ module.exports = { computeRiskLevel, requiresExpertReview, validateGovernance, generateKdnaCard, HIGH_RISK_KEYWORDS };
package/src/index.js CHANGED
@@ -20,6 +20,7 @@
20
20
  const cards = require('./cards');
21
21
  const compile = require('./compile');
22
22
  const evidence = require('./evidence');
23
+ const governance = require('./governance');
23
24
  const packaging = require('./packaging');
24
25
  const pipeline = require('./pipeline');
25
26
  const project = require('./project');
@@ -40,6 +41,7 @@ module.exports = {
40
41
  quality,
41
42
  provenance,
42
43
  pipeline,
44
+ governance,
43
45
 
44
46
  // Experimental
45
47
  evidence,
@@ -12,6 +12,7 @@
12
12
 
13
13
  const contradiction = require('./contradiction');
14
14
  const { validateAllCards } = require('./validate-cards');
15
+ const { validateGovernance } = require('../governance');
15
16
 
16
17
  function computeReadiness(project) {
17
18
  const cards = project.cards || [];
@@ -25,6 +26,12 @@ function computeReadiness(project) {
25
26
  const blocking = [];
26
27
  const warnings = [];
27
28
 
29
+ // ── Governance check (v0.6.1) ───────────────────────────────────
30
+ const govResult = validateGovernance(project);
31
+ for (const issue of govResult.issues) {
32
+ (issue.severity === 'blocking' ? blocking : warnings).push(`Governance: ${issue.message}`);
33
+ }
34
+
28
35
  // ── Card validation integration (v0.3.2) ─────────────────────────
29
36
  const cardResults = validateAllCards(project);
30
37
  for (const { card_id, issues } of cardResults) {
@@ -70,14 +77,44 @@ function computeReadiness(project) {
70
77
  const feynmanRatio = lockedAxioms.length > 0 ? lockedAxioms.filter(ax => ax.feynman_restatement).length / lockedAxioms.length : 0;
71
78
  const allFeynman = lockedAxioms.every(ax => ax.feynman_restatement) && lockedMisunderstandings.every(ms => !ms.locked || ms.feynman_restatement);
72
79
 
80
+ // Feynman quality threshold (v0.6.2)
81
+ const feynmanQuality = lockedAxioms.every(ax => {
82
+ if (!ax.feynman_restatement?.score) return false;
83
+ return ax.feynman_restatement.score.total >= 4;
84
+ });
85
+ const misunderstandingFeynmanQuality = lockedMisunderstandings.length === 0 ||
86
+ lockedMisunderstandings.every(ms => {
87
+ if (!ms.feynman_restatement?.score) return false;
88
+ return ms.feynman_restatement.score.total >= 3;
89
+ });
90
+ if (allFeynman && !feynmanQuality) {
91
+ warnings.push('Feynman: axiom restatements should score ≥4/5 for publishable grade');
92
+ }
93
+
94
+ // Compare test results requirements (v0.6.4)
95
+ const withKdnaBetter = ratedTests.filter(t => t.result === 'with_kdna_better').length;
96
+ const withoutKdnaBetter = ratedTests.filter(t => t.result === 'without_kdna_better').length;
97
+ if (ratedTests.length > 0 && withoutKdnaBetter > 0) {
98
+ warnings.push(`${withoutKdnaBetter} test(s) favored response WITHOUT KDNA — domain may not improve judgment`);
99
+ }
100
+ if (ratedTests.length > 0 && withKdnaBetter < 3 && ratedTests.length >= 5) {
101
+ warnings.push(`Only ${withKdnaBetter} tests favor KDNA — recommend ≥3 for confidence`);
102
+ }
103
+
73
104
  let grade = 'draft_grade';
74
105
  if (locked.length >= 3 && axiomsComplete && feynmanRatio >= 0.5) grade = 'human_controlled';
75
106
  if (grade === 'human_controlled' && ratedTests.length >= 5 && lockedSelfChecks.length >= 3) grade = 'tested_grade';
76
- if (grade === 'tested_grade' && ratedTests.length >= 10 && lockedAxioms.length >= 3 && lockedSelfChecks.length >= 5 && blocking.length === 0 && allFeynman) {
107
+ if (grade === 'tested_grade' && ratedTests.length >= 10 && lockedAxioms.length >= 3 && lockedSelfChecks.length >= 5 && blocking.length === 0 && allFeynman && feynmanQuality && misunderstandingFeynmanQuality) {
77
108
  grade = 'publishable_grade';
78
109
  }
79
110
 
80
- return buildResult(grade, blocking, warnings, project, { feynmanRatio, allFeynman });
111
+ // Downgrade if governance issues exist
112
+ if (grade === 'publishable_grade' && govResult && !govResult.valid) {
113
+ grade = 'tested_grade';
114
+ warnings.push('Governance checks not passed — publishable downgraded to tested');
115
+ }
116
+
117
+ return buildResult(grade, blocking, warnings, project, { feynmanRatio, allFeynman, governance: govResult });
81
118
  }
82
119
 
83
120
  function buildResult(grade, blocking, warnings, project, detail = {}) {
@@ -90,6 +127,7 @@ function buildResult(grade, blocking, warnings, project, detail = {}) {
90
127
  blocking,
91
128
  warnings,
92
129
  score: Math.max(0, 100 - blocking.length * 15 - warnings.length * 3),
130
+ governance: detail.governance || null,
93
131
  stats: {
94
132
  total_cards: (project.cards || []).length,
95
133
  locked_cards: lockedCount,
@@ -42,6 +42,22 @@ function linkTestToCards(testCase, cardIds) {
42
42
  return testCase;
43
43
  }
44
44
 
45
+ function applyTestResultsToCards(project, testCase) {
46
+ if (!testCase.result) return project;
47
+ const cards = project.cards || [];
48
+ for (const cardId of (testCase.linked_cards || [])) {
49
+ const card = cards.find(c => c.id === cardId);
50
+ if (!card) continue;
51
+ if (card.status === 'locked' && testCase.result === 'with_kdna_better') {
52
+ const { transitionCard } = require('../cards');
53
+ try {
54
+ transitionCard(card, 'tested', { by: testCase.rated_by || 'testlab', reason: `test ${testCase.id}: ${testCase.result}` });
55
+ } catch { /* card may have been already tested */ }
56
+ }
57
+ }
58
+ return project;
59
+ }
60
+
45
61
  function generateTestSummary(project) {
46
62
  const tests = project.tests || [];
47
63
  const total = tests.length;
@@ -129,14 +129,19 @@ function bumpVersion(currentVersion, bumpType) {
129
129
  }
130
130
 
131
131
  function markBreakingChange(diff) {
132
+ const recommended = recommendVersionBump(diff);
132
133
  const removedAxioms = diff.removed.filter(c => c.type === 'axiom');
133
134
  const scopeWidening = diff.changed.filter(c => c.changes && 'applies_when' in c.changes &&
134
135
  (c.changes.applies_when.after || []).length > (c.changes.applies_when.before || []).length);
136
+ const coreMeaningChanges = diff.changed.filter(c => c.changes &&
137
+ ('one_sentence' in c.changes || 'full_statement' in c.changes));
138
+
135
139
  return {
136
- breaking: removedAxioms.length > 0,
140
+ breaking: recommended === 'major',
137
141
  reason: removedAxioms.length > 0 ? `${removedAxioms.length} axiom(s) removed — breaking change` :
142
+ coreMeaningChanges.length > 0 ? `${coreMeaningChanges.length} core meaning change(s) — breaking change` :
138
143
  scopeWidening.length > 0 ? `${scopeWidening.length} scope widening(s) — may affect existing behavior` : null,
139
- recommended_bump: recommendVersionBump(diff),
144
+ recommended_bump: recommended,
140
145
  };
141
146
  }
142
147