scene-capability-engine 3.3.23 → 3.3.25
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 +58 -0
- package/bin/scene-capability-engine.js +20 -0
- package/docs/adoption-guide.md +8 -0
- package/docs/autonomous-control-guide.md +8 -8
- package/docs/command-reference.md +41 -2
- package/docs/errorbook-registry.md +12 -0
- package/lib/auto/config-schema.js +7 -7
- package/lib/commands/auto.js +2 -2
- package/lib/commands/errorbook.js +258 -0
- package/lib/commands/spec-bootstrap.js +17 -2
- package/lib/commands/spec-domain.js +217 -0
- package/lib/commands/spec-related.js +70 -0
- package/lib/commands/studio.js +345 -9
- package/lib/spec/domain-modeling.js +439 -0
- package/lib/spec/related-specs.js +260 -0
- package/lib/spec-gate/policy/default-policy.js +1 -0
- package/lib/spec-gate/rules/default-rules.js +8 -0
- package/package.json +3 -2
- package/template/.sce/steering/CORE_PRINCIPLES.md +30 -1
|
@@ -47,6 +47,11 @@ const ONTOLOGY_TAG_ALIASES = Object.freeze({
|
|
|
47
47
|
});
|
|
48
48
|
const DEFAULT_PROMOTE_MIN_QUALITY = 75;
|
|
49
49
|
const ERRORBOOK_RISK_LEVELS = Object.freeze(['low', 'medium', 'high']);
|
|
50
|
+
const DEBUG_EVIDENCE_TAGS = Object.freeze([
|
|
51
|
+
'debug-evidence',
|
|
52
|
+
'diagnostic-evidence',
|
|
53
|
+
'debug-log'
|
|
54
|
+
]);
|
|
50
55
|
const HIGH_RISK_SIGNAL_TAGS = Object.freeze([
|
|
51
56
|
'release-blocker',
|
|
52
57
|
'security',
|
|
@@ -481,6 +486,44 @@ function validateRecordPayload(payload) {
|
|
|
481
486
|
}
|
|
482
487
|
}
|
|
483
488
|
|
|
489
|
+
function hasDebugEvidenceSignals(entry = {}) {
|
|
490
|
+
const tags = normalizeStringList(entry.tags).map((item) => item.toLowerCase());
|
|
491
|
+
if (tags.some((tag) => DEBUG_EVIDENCE_TAGS.includes(tag))) {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const verificationEvidence = normalizeStringList(entry.verification_evidence);
|
|
496
|
+
if (verificationEvidence.some((item) => /^debug:/i.test(item))) {
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const sourceFiles = normalizeStringList(entry?.source?.files);
|
|
501
|
+
if (sourceFiles.some((item) => /(^|[\\/._-])(debug|trace|diagnostic|observability|telemetry|stack)/i.test(item))) {
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const notes = normalizeText(entry.notes).toLowerCase();
|
|
506
|
+
if (notes && /(debug|trace|diagnostic|observability|telemetry|stack|日志|埋点|观测)/i.test(notes)) {
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function enforceDebugEvidenceAfterRepeatedFailures(entry = {}, options = {}) {
|
|
514
|
+
const attemptCount = Number(options.attemptCount || 0);
|
|
515
|
+
if (!Number.isFinite(attemptCount) || attemptCount < 3) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
if (hasDebugEvidenceSignals(entry)) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
throw new Error(
|
|
522
|
+
'two failed fix rounds detected (attempt #3+): debug evidence is required. '
|
|
523
|
+
+ 'Provide --verification "debug: ...", add tag debug-evidence, or include debug trace/log file references.'
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
484
527
|
function normalizeRecordPayload(options = {}, fromFilePayload = {}) {
|
|
485
528
|
const temporaryMitigation = normalizeTemporaryMitigation(options, fromFilePayload);
|
|
486
529
|
const payload = {
|
|
@@ -1287,6 +1330,9 @@ async function runErrorbookRecordCommand(options = {}, dependencies = {}) {
|
|
|
1287
1330
|
throw new Error(`errorbook index references missing entry: ${existingSummary.id}`);
|
|
1288
1331
|
}
|
|
1289
1332
|
entry = mergeEntry(existingEntry, normalized);
|
|
1333
|
+
enforceDebugEvidenceAfterRepeatedFailures(entry, {
|
|
1334
|
+
attemptCount: Number(entry.occurrences || 0)
|
|
1335
|
+
});
|
|
1290
1336
|
deduplicated = true;
|
|
1291
1337
|
} else {
|
|
1292
1338
|
const temporaryMitigation = normalizeExistingTemporaryMitigation(normalized.temporary_mitigation);
|
|
@@ -1499,6 +1545,196 @@ async function runErrorbookSyncRegistryCommand(options = {}, dependencies = {})
|
|
|
1499
1545
|
return result;
|
|
1500
1546
|
}
|
|
1501
1547
|
|
|
1548
|
+
async function runErrorbookRegistryHealthCommand(options = {}, dependencies = {}) {
|
|
1549
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
1550
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
1551
|
+
const registryPaths = resolveErrorbookRegistryPaths(projectPath, {
|
|
1552
|
+
configPath: options.config,
|
|
1553
|
+
cachePath: options.cache
|
|
1554
|
+
});
|
|
1555
|
+
|
|
1556
|
+
const warnings = [];
|
|
1557
|
+
const errors = [];
|
|
1558
|
+
const overrideSource = normalizeText(options.source);
|
|
1559
|
+
const overrideIndex = normalizeText(options.index || options.registryIndex);
|
|
1560
|
+
const overrideSourceName = normalizeText(options.sourceName || options.registrySourceName) || 'override';
|
|
1561
|
+
|
|
1562
|
+
const configExists = await fileSystem.pathExists(registryPaths.configFile);
|
|
1563
|
+
if (configExists) {
|
|
1564
|
+
try {
|
|
1565
|
+
const rawConfig = await fileSystem.readJson(registryPaths.configFile);
|
|
1566
|
+
if (!rawConfig || typeof rawConfig !== 'object' || Array.isArray(rawConfig)) {
|
|
1567
|
+
errors.push(`registry config must be a JSON object: ${registryPaths.configFile}`);
|
|
1568
|
+
}
|
|
1569
|
+
} catch (error) {
|
|
1570
|
+
errors.push(`failed to parse registry config (${registryPaths.configFile}): ${error.message}`);
|
|
1571
|
+
}
|
|
1572
|
+
} else if (!overrideSource) {
|
|
1573
|
+
errors.push(`registry config file not found: ${registryPaths.configFile}`);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
const config = await readErrorbookRegistryConfig(registryPaths, fileSystem);
|
|
1577
|
+
let registryEnabled = normalizeBoolean(config.enabled, true);
|
|
1578
|
+
let sources = Array.isArray(config.sources) ? config.sources : [];
|
|
1579
|
+
|
|
1580
|
+
if (overrideSource) {
|
|
1581
|
+
registryEnabled = true;
|
|
1582
|
+
sources = [normalizeRegistrySource({
|
|
1583
|
+
name: overrideSourceName,
|
|
1584
|
+
source: overrideSource,
|
|
1585
|
+
index_url: overrideIndex
|
|
1586
|
+
})];
|
|
1587
|
+
} else if (overrideIndex && sources.length > 0) {
|
|
1588
|
+
sources = sources.map((source, sourceIndex) => (
|
|
1589
|
+
sourceIndex === 0 ? { ...source, index_url: overrideIndex } : source
|
|
1590
|
+
));
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
if (!registryEnabled) {
|
|
1594
|
+
warnings.push('registry config is disabled');
|
|
1595
|
+
}
|
|
1596
|
+
if (registryEnabled && sources.length === 0) {
|
|
1597
|
+
errors.push('registry enabled but no sources configured');
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
const maxShards = Number.isFinite(Number(options.maxShards)) && Number(options.maxShards) > 0
|
|
1601
|
+
? Number(options.maxShards)
|
|
1602
|
+
: 8;
|
|
1603
|
+
const shardSample = Number.isFinite(Number(options.shardSample)) && Number(options.shardSample) > 0
|
|
1604
|
+
? Number(options.shardSample)
|
|
1605
|
+
: 2;
|
|
1606
|
+
|
|
1607
|
+
const sourceResults = [];
|
|
1608
|
+
for (const source of sources) {
|
|
1609
|
+
const sourceName = normalizeText(source.name) || 'registry';
|
|
1610
|
+
const sourceReport = {
|
|
1611
|
+
source_name: sourceName,
|
|
1612
|
+
source: normalizeText(source.source),
|
|
1613
|
+
index_url: normalizeText(source.index_url),
|
|
1614
|
+
source_ok: false,
|
|
1615
|
+
index_ok: null,
|
|
1616
|
+
shard_sources_checked: 0,
|
|
1617
|
+
source_entries: 0,
|
|
1618
|
+
shard_entries: 0,
|
|
1619
|
+
warnings: [],
|
|
1620
|
+
errors: []
|
|
1621
|
+
};
|
|
1622
|
+
|
|
1623
|
+
if (!sourceReport.source) {
|
|
1624
|
+
sourceReport.errors.push('source is empty');
|
|
1625
|
+
} else {
|
|
1626
|
+
try {
|
|
1627
|
+
const payload = await loadRegistryPayload(projectPath, sourceReport.source, fileSystem);
|
|
1628
|
+
const entries = extractRegistryEntries(payload, sourceName);
|
|
1629
|
+
sourceReport.source_ok = true;
|
|
1630
|
+
sourceReport.source_entries = entries.length;
|
|
1631
|
+
if (entries.length === 0) {
|
|
1632
|
+
sourceReport.warnings.push('source returned no valid entries');
|
|
1633
|
+
}
|
|
1634
|
+
} catch (error) {
|
|
1635
|
+
sourceReport.errors.push(`failed to load source (${sourceReport.source}): ${error.message}`);
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
if (!sourceReport.index_url) {
|
|
1640
|
+
sourceReport.warnings.push('index_url not configured; remote indexed lookup health is partially validated');
|
|
1641
|
+
} else {
|
|
1642
|
+
try {
|
|
1643
|
+
const indexPayload = await loadRegistryPayload(projectPath, sourceReport.index_url, fileSystem);
|
|
1644
|
+
const index = normalizeRegistryIndex(indexPayload, sourceName);
|
|
1645
|
+
if (!index) {
|
|
1646
|
+
sourceReport.errors.push(`invalid index payload: ${sourceReport.index_url}`);
|
|
1647
|
+
} else {
|
|
1648
|
+
sourceReport.index_ok = true;
|
|
1649
|
+
const tokenToBucket = index.token_to_bucket || {};
|
|
1650
|
+
const unresolved = [];
|
|
1651
|
+
for (const [token, bucketRaw] of Object.entries(tokenToBucket)) {
|
|
1652
|
+
const bucket = normalizeText(bucketRaw);
|
|
1653
|
+
if (!bucket) {
|
|
1654
|
+
continue;
|
|
1655
|
+
}
|
|
1656
|
+
const bucketSource = normalizeText(index.buckets[bucket] || index.buckets[token]);
|
|
1657
|
+
if (!bucketSource) {
|
|
1658
|
+
unresolved.push(`${token}->${bucket}`);
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
if (unresolved.length > 0) {
|
|
1662
|
+
sourceReport.errors.push(`unresolved index bucket mappings: ${unresolved.slice(0, 10).join(', ')}`);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
const sampleTokens = Object.keys(tokenToBucket).slice(0, 64);
|
|
1666
|
+
const shardSources = collectRegistryShardSources(index, sampleTokens, maxShards);
|
|
1667
|
+
sourceReport.shard_sources_checked = shardSources.length;
|
|
1668
|
+
if (shardSources.length === 0) {
|
|
1669
|
+
sourceReport.warnings.push('index resolved zero shard sources');
|
|
1670
|
+
}
|
|
1671
|
+
|
|
1672
|
+
for (const shardSource of shardSources.slice(0, shardSample)) {
|
|
1673
|
+
try {
|
|
1674
|
+
const shardPayload = await loadRegistryPayload(projectPath, shardSource, fileSystem);
|
|
1675
|
+
const shardEntries = extractRegistryEntries(shardPayload, `${sourceName}-shard`);
|
|
1676
|
+
sourceReport.shard_entries += shardEntries.length;
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
sourceReport.errors.push(`failed to load shard (${shardSource}): ${error.message}`);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1682
|
+
} catch (error) {
|
|
1683
|
+
sourceReport.index_ok = false;
|
|
1684
|
+
sourceReport.errors.push(`failed to load index (${sourceReport.index_url}): ${error.message}`);
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
for (const message of sourceReport.warnings) {
|
|
1689
|
+
warnings.push(`[${sourceName}] ${message}`);
|
|
1690
|
+
}
|
|
1691
|
+
for (const message of sourceReport.errors) {
|
|
1692
|
+
errors.push(`[${sourceName}] ${message}`);
|
|
1693
|
+
}
|
|
1694
|
+
sourceResults.push(sourceReport);
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
const result = {
|
|
1698
|
+
mode: 'errorbook-health-registry',
|
|
1699
|
+
checked_at: nowIso(),
|
|
1700
|
+
passed: errors.length === 0,
|
|
1701
|
+
warning_count: warnings.length,
|
|
1702
|
+
error_count: errors.length,
|
|
1703
|
+
paths: {
|
|
1704
|
+
config_file: registryPaths.configFile,
|
|
1705
|
+
cache_file: registryPaths.cacheFile
|
|
1706
|
+
},
|
|
1707
|
+
config: {
|
|
1708
|
+
exists: configExists,
|
|
1709
|
+
enabled: registryEnabled,
|
|
1710
|
+
search_mode: config.search_mode || 'cache',
|
|
1711
|
+
source_count: sources.length
|
|
1712
|
+
},
|
|
1713
|
+
sources: sourceResults,
|
|
1714
|
+
warnings,
|
|
1715
|
+
errors
|
|
1716
|
+
};
|
|
1717
|
+
|
|
1718
|
+
if (options.json) {
|
|
1719
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1720
|
+
} else if (!options.silent) {
|
|
1721
|
+
if (result.passed) {
|
|
1722
|
+
console.log(chalk.green('✓ Errorbook registry health check passed'));
|
|
1723
|
+
} else {
|
|
1724
|
+
console.log(chalk.red('✗ Errorbook registry health check failed'));
|
|
1725
|
+
}
|
|
1726
|
+
console.log(chalk.gray(` sources: ${result.config.source_count}`));
|
|
1727
|
+
console.log(chalk.gray(` warnings: ${result.warning_count}`));
|
|
1728
|
+
console.log(chalk.gray(` errors: ${result.error_count}`));
|
|
1729
|
+
}
|
|
1730
|
+
|
|
1731
|
+
if (options.failOnAlert && !result.passed) {
|
|
1732
|
+
throw new Error(`errorbook registry health failed: ${result.error_count} error(s)`);
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
return result;
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1502
1738
|
async function runErrorbookListCommand(options = {}, dependencies = {}) {
|
|
1503
1739
|
const projectPath = dependencies.projectPath || process.cwd();
|
|
1504
1740
|
const fileSystem = dependencies.fileSystem || fs;
|
|
@@ -2148,6 +2384,26 @@ function registerErrorbookCommands(program) {
|
|
|
2148
2384
|
}
|
|
2149
2385
|
});
|
|
2150
2386
|
|
|
2387
|
+
errorbook
|
|
2388
|
+
.command('health-registry')
|
|
2389
|
+
.description('Validate external registry config/source/index health')
|
|
2390
|
+
.option('--config <path>', `Registry config path (default: ${DEFAULT_ERRORBOOK_REGISTRY_CONFIG})`)
|
|
2391
|
+
.option('--cache <path>', `Registry cache path (default: ${DEFAULT_ERRORBOOK_REGISTRY_CACHE})`)
|
|
2392
|
+
.option('--source <url-or-path>', 'Override registry source JSON (https://... or local file)')
|
|
2393
|
+
.option('--source-name <name>', 'Override source name label')
|
|
2394
|
+
.option('--index <url-or-path>', 'Override registry index source (https://... or local file)')
|
|
2395
|
+
.option('--max-shards <n>', 'Max index-resolved shards to validate', parseInt, 8)
|
|
2396
|
+
.option('--shard-sample <n>', 'Shard sample count to fetch and validate', parseInt, 2)
|
|
2397
|
+
.option('--fail-on-alert', 'Exit with error when health check finds errors')
|
|
2398
|
+
.option('--json', 'Emit machine-readable JSON')
|
|
2399
|
+
.action(async (options) => {
|
|
2400
|
+
try {
|
|
2401
|
+
await runErrorbookRegistryHealthCommand(options);
|
|
2402
|
+
} catch (error) {
|
|
2403
|
+
emitCommandError(error, options.json);
|
|
2404
|
+
}
|
|
2405
|
+
});
|
|
2406
|
+
|
|
2151
2407
|
errorbook
|
|
2152
2408
|
.command('promote <id>')
|
|
2153
2409
|
.description('Promote entry after strict quality gate')
|
|
@@ -2212,6 +2468,7 @@ module.exports = {
|
|
|
2212
2468
|
DEFAULT_ERRORBOOK_REGISTRY_CACHE,
|
|
2213
2469
|
DEFAULT_ERRORBOOK_REGISTRY_EXPORT,
|
|
2214
2470
|
HIGH_RISK_SIGNAL_TAGS,
|
|
2471
|
+
DEBUG_EVIDENCE_TAGS,
|
|
2215
2472
|
DEFAULT_PROMOTE_MIN_QUALITY,
|
|
2216
2473
|
resolveErrorbookPaths,
|
|
2217
2474
|
resolveErrorbookRegistryPaths,
|
|
@@ -2223,6 +2480,7 @@ module.exports = {
|
|
|
2223
2480
|
runErrorbookRecordCommand,
|
|
2224
2481
|
runErrorbookExportCommand,
|
|
2225
2482
|
runErrorbookSyncRegistryCommand,
|
|
2483
|
+
runErrorbookRegistryHealthCommand,
|
|
2226
2484
|
runErrorbookListCommand,
|
|
2227
2485
|
runErrorbookShowCommand,
|
|
2228
2486
|
runErrorbookFindCommand,
|
|
@@ -11,6 +11,7 @@ const { ContextCollector } = require('../spec/bootstrap/context-collector');
|
|
|
11
11
|
const { QuestionnaireEngine } = require('../spec/bootstrap/questionnaire-engine');
|
|
12
12
|
const { DraftGenerator } = require('../spec/bootstrap/draft-generator');
|
|
13
13
|
const { TraceEmitter } = require('../spec/bootstrap/trace-emitter');
|
|
14
|
+
const { ensureSpecDomainArtifacts } = require('../spec/domain-modeling');
|
|
14
15
|
const { SessionStore } = require('../runtime/session-store');
|
|
15
16
|
const { resolveSpecSceneBinding } = require('../runtime/scene-session-binding');
|
|
16
17
|
const { bindMultiSpecSceneSession } = require('../runtime/multi-spec-scene-session');
|
|
@@ -114,6 +115,14 @@ async function runSpecBootstrap(options = {}, dependencies = {}) {
|
|
|
114
115
|
await fs.writeFile(files.tasks, draft.tasks, 'utf8');
|
|
115
116
|
}
|
|
116
117
|
|
|
118
|
+
const domainArtifacts = await ensureSpecDomainArtifacts(projectPath, specName, {
|
|
119
|
+
dryRun: !!options.dryRun,
|
|
120
|
+
sceneId: sceneBinding.scene_id,
|
|
121
|
+
problemStatement: answers.problemStatement,
|
|
122
|
+
primaryFlow: answers.primaryFlow,
|
|
123
|
+
verificationPlan: answers.verificationPlan
|
|
124
|
+
});
|
|
125
|
+
|
|
117
126
|
const result = {
|
|
118
127
|
success: true,
|
|
119
128
|
specName,
|
|
@@ -122,7 +131,10 @@ async function runSpecBootstrap(options = {}, dependencies = {}) {
|
|
|
122
131
|
files: {
|
|
123
132
|
requirements: path.relative(projectPath, files.requirements),
|
|
124
133
|
design: path.relative(projectPath, files.design),
|
|
125
|
-
tasks: path.relative(projectPath, files.tasks)
|
|
134
|
+
tasks: path.relative(projectPath, files.tasks),
|
|
135
|
+
domain_map: path.relative(projectPath, domainArtifacts.paths.domain_map),
|
|
136
|
+
scene_spec: path.relative(projectPath, domainArtifacts.paths.scene_spec),
|
|
137
|
+
domain_chain: path.relative(projectPath, domainArtifacts.paths.domain_chain)
|
|
126
138
|
},
|
|
127
139
|
trace: {
|
|
128
140
|
template: options.template || 'default',
|
|
@@ -141,7 +153,10 @@ async function runSpecBootstrap(options = {}, dependencies = {}) {
|
|
|
141
153
|
preview: {
|
|
142
154
|
requirements: draft.requirements,
|
|
143
155
|
design: draft.design,
|
|
144
|
-
tasks: draft.tasks
|
|
156
|
+
tasks: draft.tasks,
|
|
157
|
+
domain_map: domainArtifacts.preview.domain_map,
|
|
158
|
+
scene_spec: domainArtifacts.preview.scene_spec,
|
|
159
|
+
domain_chain: domainArtifacts.preview.domain_chain
|
|
145
160
|
},
|
|
146
161
|
scene_session: sceneBinding
|
|
147
162
|
? {
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const {
|
|
5
|
+
ensureSpecDomainArtifacts,
|
|
6
|
+
validateSpecDomainArtifacts
|
|
7
|
+
} = require('../spec/domain-modeling');
|
|
8
|
+
|
|
9
|
+
function normalizeText(value) {
|
|
10
|
+
if (typeof value !== 'string') {
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
return value.trim();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveSpecId(options = {}) {
|
|
17
|
+
return normalizeText(options.spec || options.name);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function assertSpecExists(projectPath, specId, fileSystem = fs) {
|
|
21
|
+
const specPath = path.join(projectPath, '.sce', 'specs', specId);
|
|
22
|
+
if (!await fileSystem.pathExists(specPath)) {
|
|
23
|
+
throw new Error(`Spec not found: ${specId}`);
|
|
24
|
+
}
|
|
25
|
+
return specPath;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function runSpecDomainInitCommand(options = {}, dependencies = {}) {
|
|
29
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
30
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
31
|
+
const specId = resolveSpecId(options);
|
|
32
|
+
if (!specId) {
|
|
33
|
+
throw new Error('--spec is required');
|
|
34
|
+
}
|
|
35
|
+
await assertSpecExists(projectPath, specId, fileSystem);
|
|
36
|
+
|
|
37
|
+
const result = await ensureSpecDomainArtifacts(projectPath, specId, {
|
|
38
|
+
fileSystem,
|
|
39
|
+
dryRun: options.dryRun === true,
|
|
40
|
+
force: false,
|
|
41
|
+
sceneId: options.scene,
|
|
42
|
+
problemStatement: options.problem,
|
|
43
|
+
primaryFlow: options.primaryFlow,
|
|
44
|
+
verificationPlan: options.verificationPlan
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const payload = {
|
|
48
|
+
mode: 'spec-domain-init',
|
|
49
|
+
spec_id: specId,
|
|
50
|
+
dry_run: options.dryRun === true,
|
|
51
|
+
created: result.created,
|
|
52
|
+
files: result.paths
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
if (options.json) {
|
|
56
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
57
|
+
} else if (!options.silent) {
|
|
58
|
+
console.log(chalk.green('✓ Spec domain artifacts initialized'));
|
|
59
|
+
console.log(chalk.gray(` spec: ${specId}`));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return payload;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function runSpecDomainRefreshCommand(options = {}, dependencies = {}) {
|
|
66
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
67
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
68
|
+
const specId = resolveSpecId(options);
|
|
69
|
+
if (!specId) {
|
|
70
|
+
throw new Error('--spec is required');
|
|
71
|
+
}
|
|
72
|
+
await assertSpecExists(projectPath, specId, fileSystem);
|
|
73
|
+
|
|
74
|
+
const result = await ensureSpecDomainArtifacts(projectPath, specId, {
|
|
75
|
+
fileSystem,
|
|
76
|
+
dryRun: options.dryRun === true,
|
|
77
|
+
force: true,
|
|
78
|
+
sceneId: options.scene,
|
|
79
|
+
problemStatement: options.problem,
|
|
80
|
+
primaryFlow: options.primaryFlow,
|
|
81
|
+
verificationPlan: options.verificationPlan
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const payload = {
|
|
85
|
+
mode: 'spec-domain-refresh',
|
|
86
|
+
spec_id: specId,
|
|
87
|
+
dry_run: options.dryRun === true,
|
|
88
|
+
refreshed: result.created,
|
|
89
|
+
files: result.paths
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (options.json) {
|
|
93
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
94
|
+
} else if (!options.silent) {
|
|
95
|
+
console.log(chalk.green('✓ Spec domain artifacts refreshed'));
|
|
96
|
+
console.log(chalk.gray(` spec: ${specId}`));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return payload;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function runSpecDomainValidateCommand(options = {}, dependencies = {}) {
|
|
103
|
+
const projectPath = dependencies.projectPath || process.cwd();
|
|
104
|
+
const fileSystem = dependencies.fileSystem || fs;
|
|
105
|
+
const specId = resolveSpecId(options);
|
|
106
|
+
if (!specId) {
|
|
107
|
+
throw new Error('--spec is required');
|
|
108
|
+
}
|
|
109
|
+
await assertSpecExists(projectPath, specId, fileSystem);
|
|
110
|
+
|
|
111
|
+
const validation = await validateSpecDomainArtifacts(projectPath, specId, fileSystem);
|
|
112
|
+
const payload = {
|
|
113
|
+
mode: 'spec-domain-validate',
|
|
114
|
+
spec_id: specId,
|
|
115
|
+
passed: validation.passed,
|
|
116
|
+
ratio: validation.ratio,
|
|
117
|
+
details: validation.details,
|
|
118
|
+
warnings: validation.warnings
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
if (options.failOnError && !validation.passed) {
|
|
122
|
+
throw new Error(`spec domain validation failed: ${validation.warnings.join('; ')}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (options.json) {
|
|
126
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
127
|
+
} else if (!options.silent) {
|
|
128
|
+
if (validation.passed) {
|
|
129
|
+
console.log(chalk.green('✓ Spec domain validation passed'));
|
|
130
|
+
} else {
|
|
131
|
+
console.log(chalk.red('✗ Spec domain validation failed'));
|
|
132
|
+
validation.warnings.forEach((message) => {
|
|
133
|
+
console.log(chalk.gray(` - ${message}`));
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return payload;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function registerSpecDomainCommand(program) {
|
|
142
|
+
const specDomain = program
|
|
143
|
+
.command('spec-domain')
|
|
144
|
+
.description('Manage problem-domain modeling artifacts (use: sce spec domain)');
|
|
145
|
+
|
|
146
|
+
specDomain
|
|
147
|
+
.command('init')
|
|
148
|
+
.description('Create missing problem-domain artifacts for a Spec')
|
|
149
|
+
.requiredOption('--spec <name>', 'Spec identifier')
|
|
150
|
+
.option('--scene <scene-id>', 'Scene id used in generated chain/model')
|
|
151
|
+
.option('--problem <text>', 'Problem statement seed')
|
|
152
|
+
.option('--primary-flow <text>', 'Primary flow seed')
|
|
153
|
+
.option('--verification-plan <text>', 'Verification plan seed')
|
|
154
|
+
.option('--dry-run', 'Preview output without writing files')
|
|
155
|
+
.option('--json', 'Output machine-readable JSON')
|
|
156
|
+
.action(async (options) => {
|
|
157
|
+
try {
|
|
158
|
+
await runSpecDomainInitCommand(options);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
if (options.json) {
|
|
161
|
+
console.log(JSON.stringify({ success: false, error: error.message }, null, 2));
|
|
162
|
+
} else {
|
|
163
|
+
console.error(chalk.red('❌ spec-domain init failed:'), error.message);
|
|
164
|
+
}
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
specDomain
|
|
170
|
+
.command('refresh')
|
|
171
|
+
.description('Force-refresh problem-domain artifacts for a Spec')
|
|
172
|
+
.requiredOption('--spec <name>', 'Spec identifier')
|
|
173
|
+
.option('--scene <scene-id>', 'Scene id used in generated chain/model')
|
|
174
|
+
.option('--problem <text>', 'Problem statement seed')
|
|
175
|
+
.option('--primary-flow <text>', 'Primary flow seed')
|
|
176
|
+
.option('--verification-plan <text>', 'Verification plan seed')
|
|
177
|
+
.option('--dry-run', 'Preview output without writing files')
|
|
178
|
+
.option('--json', 'Output machine-readable JSON')
|
|
179
|
+
.action(async (options) => {
|
|
180
|
+
try {
|
|
181
|
+
await runSpecDomainRefreshCommand(options);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (options.json) {
|
|
184
|
+
console.log(JSON.stringify({ success: false, error: error.message }, null, 2));
|
|
185
|
+
} else {
|
|
186
|
+
console.error(chalk.red('❌ spec-domain refresh failed:'), error.message);
|
|
187
|
+
}
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
specDomain
|
|
193
|
+
.command('validate')
|
|
194
|
+
.description('Validate problem-domain artifacts for a Spec')
|
|
195
|
+
.requiredOption('--spec <name>', 'Spec identifier')
|
|
196
|
+
.option('--fail-on-error', 'Exit non-zero when validation fails')
|
|
197
|
+
.option('--json', 'Output machine-readable JSON')
|
|
198
|
+
.action(async (options) => {
|
|
199
|
+
try {
|
|
200
|
+
await runSpecDomainValidateCommand(options);
|
|
201
|
+
} catch (error) {
|
|
202
|
+
if (options.json) {
|
|
203
|
+
console.log(JSON.stringify({ success: false, error: error.message }, null, 2));
|
|
204
|
+
} else {
|
|
205
|
+
console.error(chalk.red('❌ spec-domain validate failed:'), error.message);
|
|
206
|
+
}
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = {
|
|
213
|
+
runSpecDomainInitCommand,
|
|
214
|
+
runSpecDomainRefreshCommand,
|
|
215
|
+
runSpecDomainValidateCommand,
|
|
216
|
+
registerSpecDomainCommand
|
|
217
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const { findRelatedSpecs } = require('../spec/related-specs');
|
|
3
|
+
|
|
4
|
+
function normalizeText(value) {
|
|
5
|
+
if (typeof value !== 'string') {
|
|
6
|
+
return '';
|
|
7
|
+
}
|
|
8
|
+
return value.trim();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function runSpecRelatedCommand(options = {}, dependencies = {}) {
|
|
12
|
+
const query = normalizeText(options.query);
|
|
13
|
+
const sceneId = normalizeText(options.scene);
|
|
14
|
+
const specId = normalizeText(options.spec);
|
|
15
|
+
|
|
16
|
+
if (!query && !sceneId && !specId) {
|
|
17
|
+
throw new Error('At least one selector is required: --query or --scene or --spec');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const payload = await findRelatedSpecs({
|
|
21
|
+
query,
|
|
22
|
+
sceneId,
|
|
23
|
+
sourceSpecId: specId,
|
|
24
|
+
limit: options.limit
|
|
25
|
+
}, dependencies);
|
|
26
|
+
|
|
27
|
+
if (options.json) {
|
|
28
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
29
|
+
} else {
|
|
30
|
+
console.log(chalk.blue('Related Specs'));
|
|
31
|
+
console.log(` Query: ${payload.query || '(none)'}`);
|
|
32
|
+
console.log(` Scene: ${payload.scene_id || '(none)'}`);
|
|
33
|
+
console.log(` Source Spec: ${payload.source_spec_id || '(none)'}`);
|
|
34
|
+
console.log(` Candidates: ${payload.total_candidates}`);
|
|
35
|
+
for (const item of payload.related_specs) {
|
|
36
|
+
console.log(` - ${item.spec_id} | score=${item.score} | scene=${item.scene_id || 'n/a'}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return payload;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function registerSpecRelatedCommand(program) {
|
|
44
|
+
program
|
|
45
|
+
.command('spec-related')
|
|
46
|
+
.description('Find previously related Specs by query/scene context')
|
|
47
|
+
.option('--query <text>', 'Problem statement or search query')
|
|
48
|
+
.option('--scene <scene-id>', 'Scene id for scene-aligned lookup')
|
|
49
|
+
.option('--spec <spec-id>', 'Use existing spec as query seed')
|
|
50
|
+
.option('--limit <n>', 'Maximum related specs to return', '5')
|
|
51
|
+
.option('--json', 'Output machine-readable JSON')
|
|
52
|
+
.action(async (options) => {
|
|
53
|
+
try {
|
|
54
|
+
await runSpecRelatedCommand(options);
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (options.json) {
|
|
57
|
+
console.log(JSON.stringify({ success: false, error: error.message }, null, 2));
|
|
58
|
+
} else {
|
|
59
|
+
console.error(chalk.red('❌ spec-related failed:'), error.message);
|
|
60
|
+
}
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
runSpecRelatedCommand,
|
|
68
|
+
registerSpecRelatedCommand
|
|
69
|
+
};
|
|
70
|
+
|