kiro-spec-engine 1.32.0 → 1.33.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/CHANGELOG.md CHANGED
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.33.0] - 2026-02-10
11
+
12
+ ### Added
13
+ - **Scene Package Directory Validation**: Comprehensive validation for scene package directories
14
+ - `kse scene package-validate --package <dir>` now supports directory-level validation
15
+ - `--strict` treat warnings as errors (exit code 1)
16
+ - `--json` structured JSON output
17
+ - Validates `scene-package.json` existence and required fields
18
+ - Contract-level validation via `validateScenePackageContract`
19
+ - Semver validation for `metadata.version` using `semver.valid`
20
+ - File existence checks for `artifacts.entry_scene` and `artifacts.generates`
21
+ - Template variable schema validation if `variables` present
22
+ - Collects all errors/warnings (no early exit)
23
+ - New `validateScenePackageDirectory` helper for programmatic use
24
+ - Follows normalize → validate → run → print pattern
25
+ - Implements Spec 83-00-scene-validate
26
+
10
27
  ## [1.32.0] - 2026-02-10
11
28
 
12
29
  ### Added
@@ -367,6 +367,7 @@ function registerSceneCommands(program) {
367
367
  .option('-p, --package <path>', 'Path to scene package contract JSON')
368
368
  .option('--spec-package <relative-path>', 'Package path inside spec folder', 'custom/scene-package.json')
369
369
  .option('--json', 'Print validation payload as JSON')
370
+ .option('--strict', 'Treat warnings as errors')
370
371
  .action(async (options) => {
371
372
  await runScenePackageValidateCommand(options);
372
373
  });
@@ -801,7 +802,8 @@ function normalizeScenePackageValidateOptions(options = {}) {
801
802
  spec: options.spec ? String(options.spec).trim() : undefined,
802
803
  packagePath: options.package ? String(options.package).trim() : undefined,
803
804
  specPackage: normalizeRelativePath(options.specPackage || 'custom/scene-package.json') || 'custom/scene-package.json',
804
- json: options.json === true
805
+ json: options.json === true,
806
+ strict: options.strict === true
805
807
  };
806
808
  }
807
809
 
@@ -5890,6 +5892,12 @@ function printScenePackageValidateSummary(options, payload, projectRoot = proces
5890
5892
  const statusLabel = payload.valid ? chalk.green('valid') : chalk.red('invalid');
5891
5893
  console.log(chalk.blue('Scene Package Validate'));
5892
5894
  console.log(` Status: ${statusLabel}`);
5895
+ if (payload.strict) {
5896
+ console.log(` Mode: ${chalk.yellow('strict')}`);
5897
+ }
5898
+ if (payload.input && payload.input.mode) {
5899
+ console.log(` Validation: ${payload.input.mode}`);
5900
+ }
5893
5901
  console.log(` Source: ${chalk.gray(resolvePath(projectRoot, payload.input.path))}`);
5894
5902
 
5895
5903
  if (payload.summary.coordinate) {
@@ -5899,6 +5907,10 @@ function printScenePackageValidateSummary(options, payload, projectRoot = proces
5899
5907
  console.log(` Kind: ${payload.summary.kind || 'unknown'}`);
5900
5908
  console.log(` Parameters: ${payload.summary.parameter_count}`);
5901
5909
 
5910
+ if (typeof payload.summary.files_checked === 'number') {
5911
+ console.log(` Files checked: ${payload.summary.files_checked}, missing: ${payload.summary.files_missing}`);
5912
+ }
5913
+
5902
5914
  if (Array.isArray(payload.errors) && payload.errors.length > 0) {
5903
5915
  console.log(chalk.red(' Errors:'));
5904
5916
  for (const error of payload.errors) {
@@ -6831,6 +6843,105 @@ async function runScenePackageTemplateCommand(rawOptions = {}, dependencies = {}
6831
6843
  }
6832
6844
  }
6833
6845
 
6846
+ async function validateScenePackageDirectory(packageDir, fileSystem) {
6847
+ const _fs = fileSystem || fs;
6848
+ const readJson = typeof _fs.readJson === 'function' ? _fs.readJson.bind(_fs) : fs.readJson.bind(fs);
6849
+ const errors = [];
6850
+ const warnings = [];
6851
+ const fileChecks = { checked: 0, missing: 0 };
6852
+
6853
+ // Read scene-package.json
6854
+ const manifestPath = path.join(packageDir, 'scene-package.json');
6855
+ let contract;
6856
+ try {
6857
+ contract = await readJson(manifestPath);
6858
+ } catch (err) {
6859
+ return {
6860
+ valid: false,
6861
+ contract: null,
6862
+ errors: [`scene-package.json not found or not valid JSON in ${packageDir}: ${err.message}`],
6863
+ warnings: [],
6864
+ fileChecks: { checked: 0, missing: 0 },
6865
+ summary: { coordinate: null, kind: null, parameter_count: 0, provides_count: 0, requires_count: 0, file_count: 0, files_checked: 0, files_missing: 0 }
6866
+ };
6867
+ }
6868
+
6869
+ // Contract-level validation
6870
+ const contractResult = validateScenePackageContract(contract);
6871
+ errors.push(...contractResult.errors);
6872
+ warnings.push(...contractResult.warnings);
6873
+
6874
+ // Semver validation (more precise than contract regex)
6875
+ const metadata = isPlainObject(contract.metadata) ? contract.metadata : {};
6876
+ const version = String(metadata.version || '').trim();
6877
+ if (version && !semver.valid(version)) {
6878
+ const hasSemverError = errors.some(e => e.includes('metadata.version'));
6879
+ if (!hasSemverError) {
6880
+ errors.push(`metadata.version "${version}" is not valid semver`);
6881
+ }
6882
+ }
6883
+
6884
+ // File existence checks
6885
+ const artifacts = isPlainObject(contract.artifacts) ? contract.artifacts : {};
6886
+ const filesToCheck = [];
6887
+ if (typeof artifacts.entry_scene === 'string' && artifacts.entry_scene.trim()) {
6888
+ filesToCheck.push(artifacts.entry_scene.trim());
6889
+ }
6890
+ if (Array.isArray(artifacts.generates)) {
6891
+ for (const g of artifacts.generates) {
6892
+ if (typeof g === 'string' && g.trim()) {
6893
+ filesToCheck.push(g.trim());
6894
+ }
6895
+ }
6896
+ }
6897
+
6898
+ for (const filePath of filesToCheck) {
6899
+ fileChecks.checked++;
6900
+ const fullPath = path.join(packageDir, filePath);
6901
+ try {
6902
+ const stat = typeof _fs.stat === 'function' ? _fs.stat.bind(_fs) : fs.stat.bind(fs);
6903
+ await stat(fullPath);
6904
+ } catch (_e) {
6905
+ fileChecks.missing++;
6906
+ errors.push(`referenced file not found: ${filePath}`);
6907
+ }
6908
+ }
6909
+
6910
+ // Template variable schema validation
6911
+ if (Array.isArray(contract.variables) && contract.variables.length > 0) {
6912
+ const varResult = validateTemplateVariableSchema(contract.variables);
6913
+ errors.push(...varResult.errors);
6914
+ warnings.push(...varResult.warnings);
6915
+ }
6916
+ if (Array.isArray(contract.parameters)) {
6917
+ for (const [index, param] of contract.parameters.entries()) {
6918
+ if (!isPlainObject(param)) continue;
6919
+ if (!String(param.id || '').trim()) {
6920
+ const hasIdError = errors.some(e => e.includes(`parameters[${index}].id`));
6921
+ if (!hasIdError) errors.push(`parameters[${index}].id is required`);
6922
+ }
6923
+ if (!String(param.type || '').trim()) {
6924
+ const hasTypeError = errors.some(e => e.includes(`parameters[${index}].type`));
6925
+ if (!hasTypeError) errors.push(`parameters[${index}].type is required`);
6926
+ }
6927
+ }
6928
+ }
6929
+
6930
+ return {
6931
+ valid: errors.length === 0,
6932
+ contract,
6933
+ errors,
6934
+ warnings,
6935
+ fileChecks,
6936
+ summary: {
6937
+ ...contractResult.summary,
6938
+ file_count: filesToCheck.length,
6939
+ files_checked: fileChecks.checked,
6940
+ files_missing: fileChecks.missing
6941
+ }
6942
+ };
6943
+ }
6944
+
6834
6945
  async function runScenePackageValidateCommand(rawOptions = {}, dependencies = {}) {
6835
6946
  const projectRoot = dependencies.projectRoot || process.cwd();
6836
6947
  const fileSystem = dependencies.fileSystem || fs;
@@ -6850,6 +6961,50 @@ async function runScenePackageValidateCommand(rawOptions = {}, dependencies = {}
6850
6961
 
6851
6962
  try {
6852
6963
  const inputPath = resolveScenePackageValidateInputPath(options, projectRoot);
6964
+
6965
+ // Detect if input is a directory for comprehensive validation
6966
+ let isDirectory = false;
6967
+ try {
6968
+ const statFn = typeof fileSystem.stat === 'function' ? fileSystem.stat.bind(fileSystem) : fs.stat.bind(fs);
6969
+ const stat = await statFn(inputPath);
6970
+ isDirectory = stat.isDirectory();
6971
+ } catch (_e) {
6972
+ // Not a directory or doesn't exist — fall through to file-based validation
6973
+ }
6974
+
6975
+ if (isDirectory) {
6976
+ const dirResult = await validateScenePackageDirectory(inputPath, fileSystem);
6977
+
6978
+ // Strict mode: promote warnings to errors
6979
+ if (options.strict && dirResult.warnings.length > 0) {
6980
+ dirResult.errors.push(...dirResult.warnings);
6981
+ dirResult.warnings = [];
6982
+ dirResult.valid = dirResult.errors.length === 0;
6983
+ }
6984
+
6985
+ const payload = {
6986
+ valid: dirResult.valid,
6987
+ input: {
6988
+ spec: options.spec || null,
6989
+ path: inputPath,
6990
+ spec_package: null,
6991
+ mode: 'directory'
6992
+ },
6993
+ summary: dirResult.summary,
6994
+ errors: dirResult.errors,
6995
+ warnings: dirResult.warnings,
6996
+ strict: options.strict
6997
+ };
6998
+
6999
+ printScenePackageValidateSummary(options, payload, projectRoot);
7000
+
7001
+ if (!dirResult.valid) {
7002
+ process.exitCode = 1;
7003
+ }
7004
+
7005
+ return payload;
7006
+ }
7007
+
6853
7008
  const packageContract = await readJson(inputPath);
6854
7009
  const validation = validateScenePackageContract(packageContract);
6855
7010
 
@@ -6858,16 +7013,24 @@ async function runScenePackageValidateCommand(rawOptions = {}, dependencies = {}
6858
7013
  input: {
6859
7014
  spec: options.spec || null,
6860
7015
  path: inputPath,
6861
- spec_package: options.spec ? options.specPackage : null
7016
+ spec_package: options.spec ? options.specPackage : null,
7017
+ mode: 'file'
6862
7018
  },
6863
7019
  summary: validation.summary,
6864
7020
  errors: validation.errors,
6865
7021
  warnings: validation.warnings
6866
7022
  };
6867
7023
 
7024
+ // Strict mode for file-based validation too
7025
+ if (options.strict && payload.warnings.length > 0) {
7026
+ payload.errors.push(...payload.warnings);
7027
+ payload.warnings = [];
7028
+ payload.valid = payload.errors.length === 0;
7029
+ }
7030
+
6868
7031
  printScenePackageValidateSummary(options, payload, projectRoot);
6869
7032
 
6870
- if (!validation.valid) {
7033
+ if (!payload.valid) {
6871
7034
  process.exitCode = 1;
6872
7035
  }
6873
7036
 
@@ -10892,5 +11055,6 @@ module.exports = {
10892
11055
  normalizeSceneInfoOptions,
10893
11056
  validateSceneInfoOptions,
10894
11057
  runSceneInfoCommand,
10895
- printSceneInfoSummary
11058
+ printSceneInfoSummary,
11059
+ validateScenePackageDirectory
10896
11060
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiro-spec-engine",
3
- "version": "1.32.0",
3
+ "version": "1.33.0",
4
4
  "description": "kiro-spec-engine (kse) - A CLI tool and npm package for spec-driven development with AI coding assistants. NOT the Kiro IDE desktop application.",
5
5
  "main": "index.js",
6
6
  "bin": {