scene-capability-engine 3.6.14 → 3.6.16
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/CHANGELOG.md +16 -0
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/docs/command-reference.md +8 -0
- package/docs/magicball-capability-library.md +8 -2
- package/lib/commands/capability.js +137 -12
- package/lib/templates/registry-parser.js +45 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.6.16] - 2026-03-06
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Capability registry validation now enforces ontology core triads for all capability templates.
|
|
14
|
+
- Capability catalog, match, and use payloads now expose `ontology_core` for direct UI rendering.
|
|
15
|
+
- Capability extract now reports triad readiness in candidate summaries.
|
|
16
|
+
|
|
17
|
+
## [3.6.15] - 2026-03-06
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- Capability plan apply result now includes preview lines + skipped titles for UI feedback.
|
|
21
|
+
- Capability iteration now enforces ontology core triads by default and exposes triad readiness in catalog/match/use payloads:
|
|
22
|
+
- entity + relation
|
|
23
|
+
- business rule
|
|
24
|
+
- decision strategy
|
|
25
|
+
|
|
10
26
|
## [3.6.14] - 2026-03-06
|
|
11
27
|
|
|
12
28
|
### Added
|
package/README.md
CHANGED
package/README.zh.md
CHANGED
|
@@ -1869,6 +1869,12 @@ sce scene template-render --package scene-erp --values '{"entity_name":"Order"}'
|
|
|
1869
1869
|
|
|
1870
1870
|
### Capability Iteration (scene -> template -> ontology)
|
|
1871
1871
|
|
|
1872
|
+
Capability candidates are now evaluated against the ontology core triad by default:
|
|
1873
|
+
- entity + relation
|
|
1874
|
+
- business rule
|
|
1875
|
+
- decision strategy
|
|
1876
|
+
|
|
1877
|
+
|
|
1872
1878
|
```bash
|
|
1873
1879
|
# 1) Extract capability candidate from scene history
|
|
1874
1880
|
sce capability extract --scene scene.customer-order --json
|
|
@@ -1890,6 +1896,8 @@ Schema references:
|
|
|
1890
1896
|
|
|
1891
1897
|
### Capability Library Reuse (query -> match -> use)
|
|
1892
1898
|
|
|
1899
|
+
`catalog/show/match/use` responses now include `ontology_core` so UI can render triad readiness directly.
|
|
1900
|
+
|
|
1893
1901
|
```bash
|
|
1894
1902
|
# List capability templates
|
|
1895
1903
|
sce capability catalog list --json
|
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
## 1. 能力库复用流程
|
|
6
6
|
|
|
7
|
+
能力模板进入能力库前,默认要求补齐本体三项核心能力:
|
|
8
|
+
- 实体关系
|
|
9
|
+
- 业务规则
|
|
10
|
+
- 决策策略
|
|
11
|
+
|
|
12
|
+
|
|
7
13
|
1. **查询**能力库(列表/搜索)
|
|
8
14
|
2. **匹配**当前 spec 的问题域(基于 problem-domain-chain 里的 ontology)
|
|
9
15
|
3. **使用**能力模板(生成可执行的 usage plan)
|
|
@@ -72,10 +78,10 @@ sce capability use --template <template-id> --spec <spec-id> --apply --json
|
|
|
72
78
|
|
|
73
79
|
### 4.2 能力库列表
|
|
74
80
|
- 支持筛选:category / risk / source
|
|
75
|
-
- 展示关键信息:`name` / `description` / `ontology_scope`
|
|
81
|
+
- 展示关键信息:`name` / `description` / `ontology_scope` / `ontology_core`
|
|
76
82
|
|
|
77
83
|
### 4.3 匹配结果
|
|
78
|
-
- 展示 `score` + `score_components`
|
|
84
|
+
- 展示 `score` + `score_components` + `ontology_core.ready`
|
|
79
85
|
- 支持一键生成 usage plan
|
|
80
86
|
|
|
81
87
|
### 4.4 使用计划
|
|
@@ -245,6 +245,7 @@ async function appendCapabilityPlanToSpecTasks(options, plan, fileSystem = fs) {
|
|
|
245
245
|
|
|
246
246
|
const lines = [];
|
|
247
247
|
const addedTasks = [];
|
|
248
|
+
const skippedTitles = [];
|
|
248
249
|
let nextTaskId = registry.maxTaskId + 1;
|
|
249
250
|
let duplicateCount = 0;
|
|
250
251
|
|
|
@@ -256,6 +257,7 @@ async function appendCapabilityPlanToSpecTasks(options, plan, fileSystem = fs) {
|
|
|
256
257
|
const titleKey = title.toLowerCase();
|
|
257
258
|
if (registry.existingTitles.has(titleKey)) {
|
|
258
259
|
duplicateCount += 1;
|
|
260
|
+
skippedTitles.push(title);
|
|
259
261
|
continue;
|
|
260
262
|
}
|
|
261
263
|
registry.existingTitles.add(titleKey);
|
|
@@ -276,8 +278,11 @@ async function appendCapabilityPlanToSpecTasks(options, plan, fileSystem = fs) {
|
|
|
276
278
|
if (addedTasks.length === 0) {
|
|
277
279
|
return {
|
|
278
280
|
tasks_path: tasksPath,
|
|
281
|
+
section_title: sectionTitle,
|
|
279
282
|
added_count: 0,
|
|
280
283
|
skipped_duplicates: duplicateCount,
|
|
284
|
+
skipped_titles: skippedTitles,
|
|
285
|
+
preview_lines: [],
|
|
281
286
|
skipped_reason: recommended.length === 0
|
|
282
287
|
? 'no recommended tasks'
|
|
283
288
|
: 'all recommended tasks already exist in tasks.md',
|
|
@@ -300,8 +305,11 @@ async function appendCapabilityPlanToSpecTasks(options, plan, fileSystem = fs) {
|
|
|
300
305
|
|
|
301
306
|
return {
|
|
302
307
|
tasks_path: tasksPath,
|
|
308
|
+
section_title: sectionTitle,
|
|
303
309
|
added_count: addedTasks.length,
|
|
304
310
|
skipped_duplicates: duplicateCount,
|
|
311
|
+
skipped_titles: skippedTitles,
|
|
312
|
+
preview_lines: lines,
|
|
305
313
|
first_task_id: addedTasks[0].task_id,
|
|
306
314
|
last_task_id: addedTasks[addedTasks.length - 1].task_id,
|
|
307
315
|
added_tasks: addedTasks
|
|
@@ -322,6 +330,83 @@ async function loadSpecDomainChain(projectPath, specId, fileSystem) {
|
|
|
322
330
|
}
|
|
323
331
|
}
|
|
324
332
|
|
|
333
|
+
function createEmptyOntologyScope() {
|
|
334
|
+
return {
|
|
335
|
+
domains: [],
|
|
336
|
+
entities: [],
|
|
337
|
+
relations: [],
|
|
338
|
+
business_rules: [],
|
|
339
|
+
decisions: []
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function normalizeOntologyScope(scope) {
|
|
344
|
+
const candidate = scope && typeof scope === 'object' ? scope : {};
|
|
345
|
+
return {
|
|
346
|
+
domains: normalizeStringArray(candidate.domains),
|
|
347
|
+
entities: normalizeStringArray(candidate.entities),
|
|
348
|
+
relations: normalizeStringArray(candidate.relations),
|
|
349
|
+
business_rules: normalizeStringArray(candidate.business_rules),
|
|
350
|
+
decisions: normalizeStringArray(candidate.decisions)
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function mergeOntologyScopes(scopes) {
|
|
355
|
+
const merged = createEmptyOntologyScope();
|
|
356
|
+
for (const scope of Array.isArray(scopes) ? scopes : []) {
|
|
357
|
+
const normalized = normalizeOntologyScope(scope);
|
|
358
|
+
for (const field of Object.keys(merged)) {
|
|
359
|
+
const combined = new Set([...(merged[field] || []), ...(normalized[field] || [])]);
|
|
360
|
+
merged[field] = Array.from(combined);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return merged;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function buildCoreOntologySummary(scope) {
|
|
367
|
+
const normalized = normalizeOntologyScope(scope);
|
|
368
|
+
const triads = {
|
|
369
|
+
entity_relation: {
|
|
370
|
+
required_fields: ['entities', 'relations'],
|
|
371
|
+
entity_count: normalized.entities.length,
|
|
372
|
+
relation_count: normalized.relations.length,
|
|
373
|
+
passed: normalized.entities.length > 0 && normalized.relations.length > 0
|
|
374
|
+
},
|
|
375
|
+
business_rules: {
|
|
376
|
+
required_fields: ['business_rules'],
|
|
377
|
+
count: normalized.business_rules.length,
|
|
378
|
+
passed: normalized.business_rules.length > 0
|
|
379
|
+
},
|
|
380
|
+
decision_strategy: {
|
|
381
|
+
required_fields: ['decisions'],
|
|
382
|
+
count: normalized.decisions.length,
|
|
383
|
+
passed: normalized.decisions.length > 0
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
const passedCount = Object.values(triads).filter((item) => item.passed).length;
|
|
387
|
+
return {
|
|
388
|
+
ontology_scope: normalized,
|
|
389
|
+
triads,
|
|
390
|
+
passed_count: passedCount,
|
|
391
|
+
total_count: 3,
|
|
392
|
+
coverage_ratio: Number((passedCount / 3).toFixed(3)),
|
|
393
|
+
ready: passedCount === 3,
|
|
394
|
+
missing: Object.entries(triads)
|
|
395
|
+
.filter(([, value]) => !value.passed)
|
|
396
|
+
.map(([key]) => key)
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function assertCoreOntologySummary(summary, contextLabel = 'capability template') {
|
|
401
|
+
const details = summary || buildCoreOntologySummary(createEmptyOntologyScope());
|
|
402
|
+
if (details.ready) {
|
|
403
|
+
return details;
|
|
404
|
+
}
|
|
405
|
+
throw new Error(
|
|
406
|
+
`${contextLabel} missing required ontology triads: ${details.missing.join(', ')}`
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
325
410
|
async function loadSceneIndexFromFile(projectPath, fileSystem) {
|
|
326
411
|
const indexPath = path.join(projectPath, '.sce', 'spec-governance', 'scene-index.json');
|
|
327
412
|
if (!await fileSystem.pathExists(indexPath)) {
|
|
@@ -496,13 +581,22 @@ function buildScoreFromCandidate(candidate) {
|
|
|
496
581
|
const reuseScore = Math.min(100, Math.round((specCount / 3) * 100));
|
|
497
582
|
const stabilityScore = Math.round(completionRate * 100);
|
|
498
583
|
const riskScore = Math.min(100, Math.round((1 - completionRate) * 100));
|
|
499
|
-
const
|
|
584
|
+
const ontologySummary = buildCoreOntologySummary(candidate && candidate.ontology_scope);
|
|
585
|
+
const ontologyCoreScore = Math.round(ontologySummary.coverage_ratio * 100);
|
|
586
|
+
const valueScore = Math.round(
|
|
587
|
+
(stabilityScore * 0.4) +
|
|
588
|
+
(reuseScore * 0.2) +
|
|
589
|
+
((100 - riskScore) * 0.1) +
|
|
590
|
+
(ontologyCoreScore * 0.3)
|
|
591
|
+
);
|
|
500
592
|
|
|
501
593
|
return {
|
|
502
594
|
completion_rate: Number(completionRate.toFixed(3)),
|
|
503
595
|
reuse_score: reuseScore,
|
|
504
596
|
stability_score: stabilityScore,
|
|
505
597
|
risk_score: riskScore,
|
|
598
|
+
ontology_core_score: ontologyCoreScore,
|
|
599
|
+
ontology_core: ontologySummary,
|
|
506
600
|
value_score: valueScore
|
|
507
601
|
};
|
|
508
602
|
}
|
|
@@ -518,15 +612,12 @@ function buildTemplateCandidate(candidate, mapping, options) {
|
|
|
518
612
|
|| `Capability template derived from ${sceneId}`;
|
|
519
613
|
const category = normalizeText(options && options.category) || 'capability';
|
|
520
614
|
const tags = normalizeStringArray(options && options.tags);
|
|
521
|
-
const ontologyScope = (
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
business_rules: [],
|
|
528
|
-
decisions: []
|
|
529
|
-
};
|
|
615
|
+
const ontologyScope = normalizeOntologyScope(
|
|
616
|
+
(mapping && mapping.ontology_scope && typeof mapping.ontology_scope === 'object')
|
|
617
|
+
? mapping.ontology_scope
|
|
618
|
+
: candidate && candidate.ontology_scope
|
|
619
|
+
);
|
|
620
|
+
const ontologyCore = buildCoreOntologySummary(ontologyScope);
|
|
530
621
|
|
|
531
622
|
return {
|
|
532
623
|
mode: 'capability-template',
|
|
@@ -538,6 +629,7 @@ function buildTemplateCandidate(candidate, mapping, options) {
|
|
|
538
629
|
scene_id: sceneId,
|
|
539
630
|
source_candidate: candidate,
|
|
540
631
|
ontology_scope: ontologyScope,
|
|
632
|
+
ontology_core: ontologyCore,
|
|
541
633
|
tags,
|
|
542
634
|
created_at: new Date().toISOString()
|
|
543
635
|
};
|
|
@@ -591,6 +683,8 @@ async function runCapabilityExtractCommand(options = {}, dependencies = {}) {
|
|
|
591
683
|
|
|
592
684
|
const taskClaimer = new TaskClaimer();
|
|
593
685
|
const specs = [];
|
|
686
|
+
const ontologyScopes = [];
|
|
687
|
+
const ontologyEvidence = [];
|
|
594
688
|
|
|
595
689
|
for (const specId of specIds) {
|
|
596
690
|
const tasksPath = path.join(projectPath, '.sce', 'specs', specId, 'tasks.md');
|
|
@@ -605,6 +699,16 @@ async function runCapabilityExtractCommand(options = {}, dependencies = {}) {
|
|
|
605
699
|
} else {
|
|
606
700
|
taskError = 'tasks.md missing';
|
|
607
701
|
}
|
|
702
|
+
const domainChain = await loadSpecDomainChain(projectPath, specId, fileSystem);
|
|
703
|
+
const specOntologyScope = domainChain.payload ? buildOntologyScopeFromChain(domainChain.payload) : createEmptyOntologyScope();
|
|
704
|
+
if (domainChain.payload) {
|
|
705
|
+
ontologyScopes.push(specOntologyScope);
|
|
706
|
+
ontologyEvidence.push({
|
|
707
|
+
spec_id: specId,
|
|
708
|
+
source: path.relative(projectPath, domainChain.path),
|
|
709
|
+
triads: buildCoreOntologySummary(specOntologyScope)
|
|
710
|
+
});
|
|
711
|
+
}
|
|
608
712
|
const taskSummary = summarizeTasks(tasks);
|
|
609
713
|
specs.push({
|
|
610
714
|
spec_id: specId,
|
|
@@ -615,10 +719,15 @@ async function runCapabilityExtractCommand(options = {}, dependencies = {}) {
|
|
|
615
719
|
title: task.title,
|
|
616
720
|
status: task.status
|
|
617
721
|
})),
|
|
722
|
+
ontology_scope: specOntologyScope,
|
|
723
|
+
ontology_source: domainChain.exists ? path.relative(projectPath, domainChain.path) : null,
|
|
724
|
+
ontology_error: domainChain.error || null,
|
|
618
725
|
task_error: taskError
|
|
619
726
|
});
|
|
620
727
|
}
|
|
621
728
|
|
|
729
|
+
const ontologyScope = mergeOntologyScopes(ontologyScopes);
|
|
730
|
+
const ontologyCore = buildCoreOntologySummary(ontologyScope);
|
|
622
731
|
const payload = {
|
|
623
732
|
mode: 'capability-extract',
|
|
624
733
|
scene_id: sceneId,
|
|
@@ -628,7 +737,15 @@ async function runCapabilityExtractCommand(options = {}, dependencies = {}) {
|
|
|
628
737
|
spec_count: specIds.length
|
|
629
738
|
},
|
|
630
739
|
specs,
|
|
631
|
-
|
|
740
|
+
ontology_scope: ontologyScope,
|
|
741
|
+
ontology_core: ontologyCore,
|
|
742
|
+
ontology_evidence: ontologyEvidence,
|
|
743
|
+
summary: {
|
|
744
|
+
...buildCandidateSummary(specs),
|
|
745
|
+
ontology_triads_ready: ontologyCore.ready,
|
|
746
|
+
ontology_triads_coverage_ratio: ontologyCore.coverage_ratio,
|
|
747
|
+
ontology_missing_triads: ontologyCore.missing
|
|
748
|
+
}
|
|
632
749
|
};
|
|
633
750
|
|
|
634
751
|
const outputPath = normalizeText(options.out) || buildDefaultCandidatePath(sceneId);
|
|
@@ -746,6 +863,11 @@ async function runCapabilityRegisterCommand(options = {}, dependencies = {}) {
|
|
|
746
863
|
if (!templateCandidate || !templateCandidate.template_id) {
|
|
747
864
|
throw new Error('template_id missing in capability template candidate');
|
|
748
865
|
}
|
|
866
|
+
const ontologySummary = assertCoreOntologySummary(
|
|
867
|
+
buildCoreOntologySummary(templateCandidate.ontology_scope),
|
|
868
|
+
'capability template'
|
|
869
|
+
);
|
|
870
|
+
templateCandidate.ontology_scope = ontologySummary.ontology_scope;
|
|
749
871
|
|
|
750
872
|
const exportDir = normalizeText(options.out) || buildDefaultExportDir(templateCandidate.template_id);
|
|
751
873
|
const outputDirAbs = path.join(projectPath, exportDir);
|
|
@@ -764,6 +886,7 @@ async function runCapabilityRegisterCommand(options = {}, dependencies = {}) {
|
|
|
764
886
|
mode: 'capability-register',
|
|
765
887
|
template_id: templateCandidate.template_id,
|
|
766
888
|
output_dir: exportDir,
|
|
889
|
+
ontology_core: ontologySummary,
|
|
767
890
|
files: [
|
|
768
891
|
path.join(exportDir, 'capability-template.json'),
|
|
769
892
|
path.join(exportDir, 'template-registry.json')
|
|
@@ -914,6 +1037,7 @@ async function matchCapabilityTemplates(options = {}) {
|
|
|
914
1037
|
description: template.description,
|
|
915
1038
|
category: template.category,
|
|
916
1039
|
risk_level: template.risk_level,
|
|
1040
|
+
ontology_core: template.ontology_core || buildCoreOntologySummary(template.ontology_scope || {}),
|
|
917
1041
|
score: Math.round(totalScore * 100),
|
|
918
1042
|
score_components: {
|
|
919
1043
|
ontology: Number(overlap.score.toFixed(3)),
|
|
@@ -998,7 +1122,8 @@ async function useCapabilityTemplate(options = {}) {
|
|
|
998
1122
|
name: template.name,
|
|
999
1123
|
source: template.source,
|
|
1000
1124
|
description: template.description,
|
|
1001
|
-
ontology_scope: template.ontology_scope || {}
|
|
1125
|
+
ontology_scope: template.ontology_scope || {},
|
|
1126
|
+
ontology_core: template.ontology_core || buildCoreOntologySummary(template.ontology_scope || {})
|
|
1002
1127
|
},
|
|
1003
1128
|
spec_id: specId,
|
|
1004
1129
|
recommended_tasks: recommendedTasks
|
|
@@ -124,12 +124,16 @@ class RegistryParser {
|
|
|
124
124
|
*/
|
|
125
125
|
normalizeTemplateEntry(template = {}) {
|
|
126
126
|
const templateType = this._resolveTemplateType(template);
|
|
127
|
+
const ontologyScope = this._normalizeOntologyScope(template.ontology_scope);
|
|
127
128
|
const normalized = {
|
|
128
129
|
...template,
|
|
129
130
|
template_type: templateType,
|
|
130
131
|
min_sce_version: template.min_sce_version ?? null,
|
|
131
132
|
max_sce_version: template.max_sce_version ?? null,
|
|
132
|
-
ontology_scope:
|
|
133
|
+
ontology_scope: ontologyScope,
|
|
134
|
+
ontology_core: templateType === 'capability-template'
|
|
135
|
+
? this._buildCapabilityOntologyCore(ontologyScope)
|
|
136
|
+
: null,
|
|
133
137
|
risk_level: template.risk_level ?? null,
|
|
134
138
|
rollback_contract: this._normalizeRollbackContract(template.rollback_contract),
|
|
135
139
|
applicable_scenarios: Array.isArray(template.applicable_scenarios) ? template.applicable_scenarios : [],
|
|
@@ -235,10 +239,10 @@ class RegistryParser {
|
|
|
235
239
|
if (!this._isPlainObject(ontologyScope)) {
|
|
236
240
|
errors.push(`${prefix}: capability-template requires "ontology_scope" object`);
|
|
237
241
|
} else {
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
if (!
|
|
241
|
-
errors.push(`${prefix}: capability-template
|
|
242
|
+
const normalizedOntologyScope = this._normalizeOntologyScope(ontologyScope);
|
|
243
|
+
const ontologyCore = this._buildCapabilityOntologyCore(normalizedOntologyScope);
|
|
244
|
+
if (!ontologyCore.ready) {
|
|
245
|
+
errors.push(`${prefix}: capability-template missing required ontology triads: ${ontologyCore.missing.join(', ')}`);
|
|
242
246
|
}
|
|
243
247
|
}
|
|
244
248
|
}
|
|
@@ -655,6 +659,42 @@ class RegistryParser {
|
|
|
655
659
|
return Object.keys(normalized).length > 0 ? normalized : null;
|
|
656
660
|
}
|
|
657
661
|
|
|
662
|
+
_buildCapabilityOntologyCore(ontologyScope) {
|
|
663
|
+
const normalized = this._normalizeOntologyScope(ontologyScope) || {};
|
|
664
|
+
const entities = Array.isArray(normalized.entities) ? normalized.entities : [];
|
|
665
|
+
const relations = Array.isArray(normalized.relations) ? normalized.relations : [];
|
|
666
|
+
const businessRules = Array.isArray(normalized.business_rules) ? normalized.business_rules : [];
|
|
667
|
+
const decisions = Array.isArray(normalized.decisions) ? normalized.decisions : [];
|
|
668
|
+
const triads = {
|
|
669
|
+
entity_relation: {
|
|
670
|
+
required_fields: ['entities', 'relations'],
|
|
671
|
+
entity_count: entities.length,
|
|
672
|
+
relation_count: relations.length,
|
|
673
|
+
passed: entities.length > 0 && relations.length > 0
|
|
674
|
+
},
|
|
675
|
+
business_rules: {
|
|
676
|
+
required_fields: ['business_rules'],
|
|
677
|
+
count: businessRules.length,
|
|
678
|
+
passed: businessRules.length > 0
|
|
679
|
+
},
|
|
680
|
+
decision_strategy: {
|
|
681
|
+
required_fields: ['decisions'],
|
|
682
|
+
count: decisions.length,
|
|
683
|
+
passed: decisions.length > 0
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
const missing = Object.entries(triads)
|
|
687
|
+
.filter(([, value]) => !value.passed)
|
|
688
|
+
.map(([key]) => key);
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
triads,
|
|
692
|
+
ready: missing.length === 0,
|
|
693
|
+
missing,
|
|
694
|
+
coverage_ratio: Number(((3 - missing.length) / 3).toFixed(3))
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
658
698
|
/**
|
|
659
699
|
* Normalizes rollback contract
|
|
660
700
|
*
|
package/package.json
CHANGED