kiro-spec-engine 1.47.12 → 1.47.15
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 +7 -0
- package/README.md +7 -0
- package/README.zh.md +7 -0
- package/docs/command-reference.md +8 -0
- package/lib/commands/orchestrate.js +14 -0
- package/lib/commands/scene.js +14 -0
- package/lib/orchestrator/orchestration-engine.js +83 -5
- package/lib/orchestrator/status-monitor.js +107 -0
- package/lib/scene-runtime/binding-registry.js +17 -1
- package/lib/scene-runtime/moqui-adapter.js +45 -6
- package/lib/scene-runtime/moqui-client.js +93 -9
- package/lib/scene-runtime/moqui-extractor.js +385 -67
- package/lib/scene-runtime/runtime-executor.js +5 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
|
+
- **Moqui runtime binding config overrides**: Added `--moqui-config <path>` to `kse scene run` and `kse scene doctor`, allowing runtime binding resolution to use an explicit `moqui-adapter.json` path per execution context.
|
|
12
|
+
- **Moqui client rate-limit resilience tests**: Added dedicated unit coverage for `429 Too Many Requests` retry/exhaustion handling and retryable network error recovery in `tests/unit/scene-runtime/moqui-client.test.js`.
|
|
13
|
+
- **Template ontology contract completeness**: Hardened scene package template contract examples to include ontology entities/relations plus governance lineage/rules/decision sections required by strict lint and ontology validation flows.
|
|
11
14
|
- **Scene ontology impact/path analysis commands**: Added `kse scene ontology impact` (reverse dependency blast-radius analysis with `--relation` and `--max-depth`) and `kse scene ontology path` (shortest relation path between refs with optional `--undirected`) to improve ontology-driven change planning and explainability.
|
|
12
15
|
- **Close-loop quantitative DoD gates**: Added `--dod-max-risk-level`, `--dod-kpi-min-completion-rate`, `--dod-max-success-rate-drop`, and `--dod-baseline-window` so autonomous closure can enforce explicit risk/KPI/baseline thresholds beyond binary checks.
|
|
13
16
|
- **Close-loop conflict/ontology execution planning**: Added lease-conflict scheduling governance and scene ontology scheduling guidance with opt-out controls (`--no-conflict-governance`, `--no-ontology-guidance`), and surfaced planning telemetry in `portfolio.execution_plan` plus agent sync plan output.
|
|
@@ -114,6 +117,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
114
117
|
- **Live orchestration status streaming**: Added event/interval-driven status persistence callback support in `runOrchestration()` and wired `auto close-loop` live progress output (`--no-stream` to disable).
|
|
115
118
|
|
|
116
119
|
### Changed
|
|
120
|
+
- **Default ERP binding routing now prefers Moqui when configured**: Runtime default handler order now resolves `spec.erp.*` through `moqui.adapter` first when adapter config is present, while preserving deterministic fallback to `builtin.erp-sim` when config is unavailable.
|
|
121
|
+
- **Moqui extraction output enriched for AI-native ontology usage**: Extracted manifests/contracts now emit action intent semantics, dependency chains, governance lineage, ontology model entities/relations, and agent hints for downstream planning.
|
|
117
122
|
- **Controller queue hygiene default**: `close-loop-controller` now deduplicates duplicate broad goals by default (`--no-controller-dedupe` to preserve raw duplicates), and summary telemetry includes dedupe/lock/session metadata.
|
|
118
123
|
- **Positioning and onboarding messaging**: Strengthened EN/ZH README and quick-start docs with explicit kse advantage matrix, 90-second value proof, and KPI observability positioning to improve first-contact clarity.
|
|
119
124
|
- **CLI first-screen positioning text**: Updated `kse --help` top description in EN/ZH locales to reflect current core strengths: Spec workflow, orchestration, and KPI observability.
|
|
@@ -127,6 +132,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
127
132
|
- **Autonomous operator UX**: Expanded docs with semantic decomposition and live stream behavior for close-loop command usage.
|
|
128
133
|
|
|
129
134
|
### Fixed
|
|
135
|
+
- **Scene runtime 429 failure behavior under high request pressure**: Moqui HTTP client now retries on `429` with `Retry-After` support and bounded exponential backoff, reducing multi-agent stalls caused by transient service-side request limits.
|
|
136
|
+
- **Moqui adapter matching fallback safety**: `spec.erp.*` bindings no longer get captured by Moqui handler when no adapter config exists, preventing false hard-fail paths and restoring expected simulator fallback behavior.
|
|
130
137
|
- **Controller summary semantics**: `close-loop-controller` now reports final `pending_goals` from the persisted queue snapshot and only marks `cycle-limit-reached` as exhausted when pending work remains (or empty-polling mode explicitly consumes cycle budget).
|
|
131
138
|
- **npm package hygiene**: Excluded transient Python bytecode artifacts (`__pycache__`, `*.pyc/pyo/pyd`) from published package contents to reduce package noise and size.
|
|
132
139
|
- **Documentation contact placeholders**: Replaced `yourusername` placeholder repository links in onboarding docs with the canonical project URL and removed stale example email contact.
|
package/README.md
CHANGED
|
@@ -413,6 +413,12 @@ Tip: `kse spec bootstrap|pipeline run|gate run --specs ...` now defaults to this
|
|
|
413
413
|
"maxParallel": 3,
|
|
414
414
|
"timeoutSeconds": 900,
|
|
415
415
|
"maxRetries": 2,
|
|
416
|
+
"rateLimitMaxRetries": 6,
|
|
417
|
+
"rateLimitBackoffBaseMs": 1000,
|
|
418
|
+
"rateLimitBackoffMaxMs": 30000,
|
|
419
|
+
"rateLimitAdaptiveParallel": true,
|
|
420
|
+
"rateLimitParallelFloor": 1,
|
|
421
|
+
"rateLimitCooldownMs": 30000,
|
|
416
422
|
"apiKeyEnvVar": "CODEX_API_KEY",
|
|
417
423
|
"codexArgs": ["--skip-git-repo-check"],
|
|
418
424
|
"codexCommand": "npx @openai/codex"
|
|
@@ -420,6 +426,7 @@ Tip: `kse spec bootstrap|pipeline run|gate run --specs ...` now defaults to this
|
|
|
420
426
|
```
|
|
421
427
|
|
|
422
428
|
If Codex CLI is globally installed, you can set `"codexCommand": "codex"`.
|
|
429
|
+
Use the `rateLimit*` settings to absorb transient 429/too-many-requests failures without stalling orchestration.
|
|
423
430
|
|
|
424
431
|
### Spec-Level Steering & Context Sync 🚀 NEW in v1.44.0
|
|
425
432
|
- **Spec Steering (L4)**: Independent `steering.md` per Spec with constraints, notes, and decisions — zero cross-agent conflict
|
package/README.zh.md
CHANGED
|
@@ -367,6 +367,12 @@ kse orchestrate stop
|
|
|
367
367
|
"maxParallel": 3,
|
|
368
368
|
"timeoutSeconds": 900,
|
|
369
369
|
"maxRetries": 2,
|
|
370
|
+
"rateLimitMaxRetries": 6,
|
|
371
|
+
"rateLimitBackoffBaseMs": 1000,
|
|
372
|
+
"rateLimitBackoffMaxMs": 30000,
|
|
373
|
+
"rateLimitAdaptiveParallel": true,
|
|
374
|
+
"rateLimitParallelFloor": 1,
|
|
375
|
+
"rateLimitCooldownMs": 30000,
|
|
370
376
|
"apiKeyEnvVar": "CODEX_API_KEY",
|
|
371
377
|
"codexArgs": ["--skip-git-repo-check"],
|
|
372
378
|
"codexCommand": "npx @openai/codex"
|
|
@@ -374,6 +380,7 @@ kse orchestrate stop
|
|
|
374
380
|
```
|
|
375
381
|
|
|
376
382
|
如果你已全局安装 Codex CLI,可将 `"codexCommand"` 改为 `"codex"`。
|
|
383
|
+
可通过 `rateLimit*` 配置吸收 429/too-many-requests 等限流抖动,避免编排流程卡死。
|
|
377
384
|
|
|
378
385
|
### Spec 级 Steering 与上下文同步 🚀 v1.44.0 新增
|
|
379
386
|
- **Spec Steering (L4)**: 每个 Spec 独立的 `steering.md`,包含约束、注意事项、决策记录 — 跨 Agent 零冲突
|
|
@@ -787,12 +787,20 @@ Recommended `.kiro/config/orchestrator.json`:
|
|
|
787
787
|
"maxParallel": 3,
|
|
788
788
|
"timeoutSeconds": 900,
|
|
789
789
|
"maxRetries": 2,
|
|
790
|
+
"rateLimitMaxRetries": 6,
|
|
791
|
+
"rateLimitBackoffBaseMs": 1000,
|
|
792
|
+
"rateLimitBackoffMaxMs": 30000,
|
|
793
|
+
"rateLimitAdaptiveParallel": true,
|
|
794
|
+
"rateLimitParallelFloor": 1,
|
|
795
|
+
"rateLimitCooldownMs": 30000,
|
|
790
796
|
"apiKeyEnvVar": "CODEX_API_KEY",
|
|
791
797
|
"codexArgs": ["--skip-git-repo-check"],
|
|
792
798
|
"codexCommand": "npx @openai/codex"
|
|
793
799
|
}
|
|
794
800
|
```
|
|
795
801
|
|
|
802
|
+
`rateLimit*` settings provide dedicated retry/backoff and adaptive parallel throttling when providers return 429 / too-many-requests errors.
|
|
803
|
+
|
|
796
804
|
### Scene Template Engine
|
|
797
805
|
|
|
798
806
|
```bash
|
|
@@ -124,6 +124,9 @@ async function runOrchestration(options = {}, dependencies = {}) {
|
|
|
124
124
|
'spec:start',
|
|
125
125
|
'spec:complete',
|
|
126
126
|
'spec:failed',
|
|
127
|
+
'spec:rate-limited',
|
|
128
|
+
'parallel:throttled',
|
|
129
|
+
'parallel:recovered',
|
|
127
130
|
'orchestration:complete'
|
|
128
131
|
];
|
|
129
132
|
|
|
@@ -322,6 +325,17 @@ function _printStatus(status) {
|
|
|
322
325
|
if (status.currentBatch !== undefined && status.totalBatches !== undefined) {
|
|
323
326
|
console.log(`Batch: ${status.currentBatch} / ${status.totalBatches}`);
|
|
324
327
|
}
|
|
328
|
+
if (status.parallel) {
|
|
329
|
+
const effective = status.parallel.effectiveMaxParallel ?? '-';
|
|
330
|
+
const max = status.parallel.maxParallel ?? '-';
|
|
331
|
+
const adaptive = status.parallel.adaptive === false ? 'off' : 'on';
|
|
332
|
+
console.log(`Parallel: ${effective} / ${max} (adaptive: ${adaptive})`);
|
|
333
|
+
}
|
|
334
|
+
if (status.rateLimit) {
|
|
335
|
+
const signals = status.rateLimit.signalCount || 0;
|
|
336
|
+
const backoff = status.rateLimit.totalBackoffMs || 0;
|
|
337
|
+
console.log(`Rate-limit: ${signals} signal(s), total backoff ${backoff}ms`);
|
|
338
|
+
}
|
|
325
339
|
if (status.specs) {
|
|
326
340
|
console.log('');
|
|
327
341
|
for (const [name, info] of Object.entries(status.specs)) {
|
package/lib/commands/scene.js
CHANGED
|
@@ -183,6 +183,7 @@ function registerSceneCommands(program) {
|
|
|
183
183
|
.option('--safety-preflight', 'Set context.safetyChecks.preflight=true')
|
|
184
184
|
.option('--safety-stop-channel', 'Set context.safetyChecks.stopChannel=true')
|
|
185
185
|
.option('--check-adapter', 'Run adapter readiness check for robot/hybrid scene')
|
|
186
|
+
.option('--moqui-config <path>', 'Path to moqui-adapter.json config file for runtime bindings')
|
|
186
187
|
.option('--binding-plugin-dir <path>', 'Binding plugin directory for runtime handler loading')
|
|
187
188
|
.option('--binding-plugin-manifest <path>', 'Binding plugin manifest JSON path')
|
|
188
189
|
.option('--no-binding-plugin-auto-discovery', 'Disable runtime binding plugin auto-discovery under .kiro')
|
|
@@ -226,6 +227,7 @@ function registerSceneCommands(program) {
|
|
|
226
227
|
.option('--allow-hybrid-commit', 'Set context.allowHybridCommit=true')
|
|
227
228
|
.option('--safety-preflight', 'Set context.safetyChecks.preflight=true')
|
|
228
229
|
.option('--safety-stop-channel', 'Set context.safetyChecks.stopChannel=true')
|
|
230
|
+
.option('--moqui-config <path>', 'Path to moqui-adapter.json config file for runtime bindings')
|
|
229
231
|
.option('--binding-plugin-dir <path>', 'Binding plugin directory for runtime handler loading')
|
|
230
232
|
.option('--binding-plugin-manifest <path>', 'Binding plugin manifest JSON path')
|
|
231
233
|
.option('--no-binding-plugin-auto-discovery', 'Disable runtime binding plugin auto-discovery under .kiro')
|
|
@@ -929,6 +931,7 @@ function normalizeRunOptions(options = {}) {
|
|
|
929
931
|
...context,
|
|
930
932
|
mode: options.mode || 'dry_run',
|
|
931
933
|
traceId: options.traceId,
|
|
934
|
+
moquiConfig: options.moquiConfig,
|
|
932
935
|
bindingPluginDir: options.bindingPluginDir,
|
|
933
936
|
bindingPluginManifest: options.bindingPluginManifest,
|
|
934
937
|
bindingPluginAutoDiscovery: options.bindingPluginAutoDiscovery !== false,
|
|
@@ -952,6 +955,7 @@ function normalizeDoctorOptions(options = {}) {
|
|
|
952
955
|
mode: options.mode || 'dry_run',
|
|
953
956
|
traceId: options.traceId,
|
|
954
957
|
checkAdapter: options.checkAdapter === true,
|
|
958
|
+
moquiConfig: options.moquiConfig,
|
|
955
959
|
bindingPluginDir: options.bindingPluginDir,
|
|
956
960
|
bindingPluginManifest: options.bindingPluginManifest,
|
|
957
961
|
bindingPluginAutoDiscovery: options.bindingPluginAutoDiscovery !== false,
|
|
@@ -1226,6 +1230,10 @@ function validateRunOptions(options) {
|
|
|
1226
1230
|
return '--binding-plugin-manifest must be a non-empty path';
|
|
1227
1231
|
}
|
|
1228
1232
|
|
|
1233
|
+
if (options.moquiConfig && (typeof options.moquiConfig !== 'string' || String(options.moquiConfig).trim().length === 0)) {
|
|
1234
|
+
return '--moqui-config must be a non-empty path';
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1229
1237
|
return null;
|
|
1230
1238
|
}
|
|
1231
1239
|
|
|
@@ -1252,6 +1260,10 @@ function validateDoctorOptions(options) {
|
|
|
1252
1260
|
return '--binding-plugin-manifest must be a non-empty path';
|
|
1253
1261
|
}
|
|
1254
1262
|
|
|
1263
|
+
if (options.moquiConfig && (typeof options.moquiConfig !== 'string' || String(options.moquiConfig).trim().length === 0)) {
|
|
1264
|
+
return '--moqui-config must be a non-empty path';
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1255
1267
|
return null;
|
|
1256
1268
|
}
|
|
1257
1269
|
|
|
@@ -6478,6 +6490,7 @@ async function runSceneDoctorCommand(rawOptions = {}, dependencies = {}) {
|
|
|
6478
6490
|
|
|
6479
6491
|
const runtimeExecutor = dependencies.runtimeExecutor || new RuntimeExecutor({
|
|
6480
6492
|
projectRoot,
|
|
6493
|
+
moquiConfigPath: options.moquiConfig,
|
|
6481
6494
|
bindingPluginDir: options.bindingPluginDir,
|
|
6482
6495
|
bindingPluginManifest: options.bindingPluginManifest,
|
|
6483
6496
|
bindingPluginAutoDiscovery: options.bindingPluginAutoDiscovery,
|
|
@@ -8234,6 +8247,7 @@ async function runSceneCommand(rawOptions = {}, dependencies = {}) {
|
|
|
8234
8247
|
|
|
8235
8248
|
const runtimeExecutor = dependencies.runtimeExecutor || new RuntimeExecutor({
|
|
8236
8249
|
projectRoot,
|
|
8250
|
+
moquiConfigPath: options.moquiConfig,
|
|
8237
8251
|
bindingPluginDir: options.bindingPluginDir,
|
|
8238
8252
|
bindingPluginManifest: options.bindingPluginManifest,
|
|
8239
8253
|
bindingPluginAutoDiscovery: options.bindingPluginAutoDiscovery,
|
|
@@ -463,6 +463,12 @@ class OrchestrationEngine extends EventEmitter {
|
|
|
463
463
|
: 0;
|
|
464
464
|
if (retryDelayMs > 0) {
|
|
465
465
|
this._onRateLimitSignal();
|
|
466
|
+
this._updateStatusMonitorRateLimit({
|
|
467
|
+
specName,
|
|
468
|
+
retryCount,
|
|
469
|
+
retryDelayMs,
|
|
470
|
+
error: resolvedError,
|
|
471
|
+
});
|
|
466
472
|
this.emit('spec:rate-limited', {
|
|
467
473
|
specName,
|
|
468
474
|
retryCount,
|
|
@@ -650,6 +656,15 @@ class OrchestrationEngine extends EventEmitter {
|
|
|
650
656
|
this._baseMaxParallel = boundedMax;
|
|
651
657
|
this._effectiveMaxParallel = boundedMax;
|
|
652
658
|
this._rateLimitCooldownUntil = 0;
|
|
659
|
+
this._updateStatusMonitorParallelTelemetry({
|
|
660
|
+
adaptive: this._isAdaptiveParallelEnabled(),
|
|
661
|
+
maxParallel: boundedMax,
|
|
662
|
+
effectiveMaxParallel: boundedMax,
|
|
663
|
+
floor: Math.min(
|
|
664
|
+
boundedMax,
|
|
665
|
+
this._toPositiveInteger(this._rateLimitParallelFloor, DEFAULT_RATE_LIMIT_PARALLEL_FLOOR)
|
|
666
|
+
),
|
|
667
|
+
});
|
|
653
668
|
}
|
|
654
669
|
|
|
655
670
|
/**
|
|
@@ -659,20 +674,35 @@ class OrchestrationEngine extends EventEmitter {
|
|
|
659
674
|
*/
|
|
660
675
|
_getEffectiveMaxParallel(maxParallel) {
|
|
661
676
|
const boundedMax = this._toPositiveInteger(maxParallel, 1);
|
|
677
|
+
const floor = Math.min(
|
|
678
|
+
boundedMax,
|
|
679
|
+
this._toPositiveInteger(this._rateLimitParallelFloor, DEFAULT_RATE_LIMIT_PARALLEL_FLOOR)
|
|
680
|
+
);
|
|
681
|
+
|
|
662
682
|
if (!this._isAdaptiveParallelEnabled()) {
|
|
663
683
|
this._baseMaxParallel = boundedMax;
|
|
684
|
+
this._effectiveMaxParallel = boundedMax;
|
|
685
|
+
this._updateStatusMonitorParallelTelemetry({
|
|
686
|
+
adaptive: false,
|
|
687
|
+
maxParallel: boundedMax,
|
|
688
|
+
effectiveMaxParallel: boundedMax,
|
|
689
|
+
floor,
|
|
690
|
+
});
|
|
664
691
|
return boundedMax;
|
|
665
692
|
}
|
|
666
693
|
|
|
667
694
|
this._baseMaxParallel = boundedMax;
|
|
668
695
|
this._maybeRecoverParallelLimit(boundedMax);
|
|
669
696
|
|
|
670
|
-
const floor = Math.min(
|
|
671
|
-
boundedMax,
|
|
672
|
-
this._toPositiveInteger(this._rateLimitParallelFloor, DEFAULT_RATE_LIMIT_PARALLEL_FLOOR)
|
|
673
|
-
);
|
|
674
697
|
const effective = this._toPositiveInteger(this._effectiveMaxParallel, boundedMax);
|
|
675
|
-
|
|
698
|
+
const resolved = Math.max(floor, Math.min(boundedMax, effective));
|
|
699
|
+
this._updateStatusMonitorParallelTelemetry({
|
|
700
|
+
adaptive: true,
|
|
701
|
+
maxParallel: boundedMax,
|
|
702
|
+
effectiveMaxParallel: resolved,
|
|
703
|
+
floor,
|
|
704
|
+
});
|
|
705
|
+
return resolved;
|
|
676
706
|
}
|
|
677
707
|
|
|
678
708
|
/**
|
|
@@ -693,6 +723,14 @@ class OrchestrationEngine extends EventEmitter {
|
|
|
693
723
|
|
|
694
724
|
if (next < current) {
|
|
695
725
|
this._effectiveMaxParallel = next;
|
|
726
|
+
this._updateStatusMonitorParallelTelemetry({
|
|
727
|
+
event: 'throttled',
|
|
728
|
+
reason: 'rate-limit',
|
|
729
|
+
adaptive: true,
|
|
730
|
+
maxParallel: base,
|
|
731
|
+
effectiveMaxParallel: next,
|
|
732
|
+
floor,
|
|
733
|
+
});
|
|
696
734
|
this.emit('parallel:throttled', {
|
|
697
735
|
reason: 'rate-limit',
|
|
698
736
|
previousMaxParallel: current,
|
|
@@ -730,6 +768,12 @@ class OrchestrationEngine extends EventEmitter {
|
|
|
730
768
|
if (next > current) {
|
|
731
769
|
this._effectiveMaxParallel = next;
|
|
732
770
|
this._rateLimitCooldownUntil = this._getNow() + this._rateLimitCooldownMs;
|
|
771
|
+
this._updateStatusMonitorParallelTelemetry({
|
|
772
|
+
event: 'recovered',
|
|
773
|
+
adaptive: true,
|
|
774
|
+
maxParallel: boundedMax,
|
|
775
|
+
effectiveMaxParallel: next,
|
|
776
|
+
});
|
|
733
777
|
this.emit('parallel:recovered', {
|
|
734
778
|
previousMaxParallel: current,
|
|
735
779
|
effectiveMaxParallel: next,
|
|
@@ -841,6 +885,40 @@ class OrchestrationEngine extends EventEmitter {
|
|
|
841
885
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
842
886
|
}
|
|
843
887
|
|
|
888
|
+
/**
|
|
889
|
+
* Safely update StatusMonitor rate-limit telemetry.
|
|
890
|
+
*
|
|
891
|
+
* @param {object} payload
|
|
892
|
+
* @private
|
|
893
|
+
*/
|
|
894
|
+
_updateStatusMonitorRateLimit(payload) {
|
|
895
|
+
const handler = this._statusMonitor && this._statusMonitor.recordRateLimitEvent;
|
|
896
|
+
if (typeof handler === 'function') {
|
|
897
|
+
try {
|
|
898
|
+
handler.call(this._statusMonitor, payload);
|
|
899
|
+
} catch (_err) {
|
|
900
|
+
// Non-fatal status telemetry update.
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Safely update StatusMonitor adaptive parallel telemetry.
|
|
907
|
+
*
|
|
908
|
+
* @param {object} payload
|
|
909
|
+
* @private
|
|
910
|
+
*/
|
|
911
|
+
_updateStatusMonitorParallelTelemetry(payload) {
|
|
912
|
+
const handler = this._statusMonitor && this._statusMonitor.updateParallelTelemetry;
|
|
913
|
+
if (typeof handler === 'function') {
|
|
914
|
+
try {
|
|
915
|
+
handler.call(this._statusMonitor, payload);
|
|
916
|
+
} catch (_err) {
|
|
917
|
+
// Non-fatal status telemetry update.
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
|
|
844
922
|
/**
|
|
845
923
|
* Validate that all spec directories exist (Req 6.4).
|
|
846
924
|
*
|
|
@@ -56,6 +56,37 @@ class StatusMonitor {
|
|
|
56
56
|
* @type {Map<string, {status: string, batch: number, agentId: string|null, retryCount: number, error: string|null, turnCount: number}>}
|
|
57
57
|
*/
|
|
58
58
|
this._specs = new Map();
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Rate-limit telemetry summary.
|
|
62
|
+
* @type {{signalCount: number, retryCount: number, totalBackoffMs: number, lastSignalAt: string|null, lastSpecName: string|null, lastRetryCount: number, lastDelayMs: number, lastError: string|null}}
|
|
63
|
+
*/
|
|
64
|
+
this._rateLimit = {
|
|
65
|
+
signalCount: 0,
|
|
66
|
+
retryCount: 0,
|
|
67
|
+
totalBackoffMs: 0,
|
|
68
|
+
lastSignalAt: null,
|
|
69
|
+
lastSpecName: null,
|
|
70
|
+
lastRetryCount: 0,
|
|
71
|
+
lastDelayMs: 0,
|
|
72
|
+
lastError: null,
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parallelism telemetry summary.
|
|
77
|
+
* @type {{adaptive: boolean|null, maxParallel: number|null, effectiveMaxParallel: number|null, floor: number|null, throttledCount: number, recoveredCount: number, lastReason: string|null, lastThrottledAt: string|null, lastRecoveredAt: string|null}}
|
|
78
|
+
*/
|
|
79
|
+
this._parallel = {
|
|
80
|
+
adaptive: null,
|
|
81
|
+
maxParallel: null,
|
|
82
|
+
effectiveMaxParallel: null,
|
|
83
|
+
floor: null,
|
|
84
|
+
throttledCount: 0,
|
|
85
|
+
recoveredCount: 0,
|
|
86
|
+
lastReason: null,
|
|
87
|
+
lastThrottledAt: null,
|
|
88
|
+
lastRecoveredAt: null,
|
|
89
|
+
};
|
|
59
90
|
}
|
|
60
91
|
|
|
61
92
|
// ---------------------------------------------------------------------------
|
|
@@ -125,6 +156,80 @@ class StatusMonitor {
|
|
|
125
156
|
}
|
|
126
157
|
}
|
|
127
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Record a rate-limit retry/backoff signal.
|
|
161
|
+
*
|
|
162
|
+
* @param {object} data
|
|
163
|
+
* @param {string} [data.specName]
|
|
164
|
+
* @param {number} [data.retryCount]
|
|
165
|
+
* @param {number} [data.retryDelayMs]
|
|
166
|
+
* @param {string} [data.error]
|
|
167
|
+
*/
|
|
168
|
+
recordRateLimitEvent(data = {}) {
|
|
169
|
+
const delay = Number(data.retryDelayMs);
|
|
170
|
+
const retryCount = Number(data.retryCount);
|
|
171
|
+
|
|
172
|
+
this._rateLimit.signalCount++;
|
|
173
|
+
this._rateLimit.retryCount++;
|
|
174
|
+
this._rateLimit.totalBackoffMs += Number.isFinite(delay) && delay > 0 ? Math.round(delay) : 0;
|
|
175
|
+
this._rateLimit.lastSignalAt = new Date().toISOString();
|
|
176
|
+
|
|
177
|
+
if (typeof data.specName === 'string' && data.specName.trim()) {
|
|
178
|
+
this._rateLimit.lastSpecName = data.specName.trim();
|
|
179
|
+
}
|
|
180
|
+
if (Number.isFinite(retryCount) && retryCount >= 0) {
|
|
181
|
+
this._rateLimit.lastRetryCount = Math.floor(retryCount);
|
|
182
|
+
}
|
|
183
|
+
this._rateLimit.lastDelayMs = Number.isFinite(delay) && delay > 0 ? Math.round(delay) : 0;
|
|
184
|
+
if (data.error !== undefined && data.error !== null) {
|
|
185
|
+
this._rateLimit.lastError = `${data.error}`;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Update adaptive parallel telemetry.
|
|
191
|
+
*
|
|
192
|
+
* @param {object} data
|
|
193
|
+
* @param {'throttled'|'recovered'} [data.event]
|
|
194
|
+
* @param {string} [data.reason]
|
|
195
|
+
* @param {number} [data.maxParallel]
|
|
196
|
+
* @param {number} [data.effectiveMaxParallel]
|
|
197
|
+
* @param {number} [data.floor]
|
|
198
|
+
* @param {boolean} [data.adaptive]
|
|
199
|
+
*/
|
|
200
|
+
updateParallelTelemetry(data = {}) {
|
|
201
|
+
if (typeof data.adaptive === 'boolean') {
|
|
202
|
+
this._parallel.adaptive = data.adaptive;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const maxParallel = Number(data.maxParallel);
|
|
206
|
+
if (Number.isFinite(maxParallel) && maxParallel > 0) {
|
|
207
|
+
this._parallel.maxParallel = Math.floor(maxParallel);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const effective = Number(data.effectiveMaxParallel);
|
|
211
|
+
if (Number.isFinite(effective) && effective > 0) {
|
|
212
|
+
this._parallel.effectiveMaxParallel = Math.floor(effective);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const floor = Number(data.floor);
|
|
216
|
+
if (Number.isFinite(floor) && floor > 0) {
|
|
217
|
+
this._parallel.floor = Math.floor(floor);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (typeof data.reason === 'string' && data.reason.trim()) {
|
|
221
|
+
this._parallel.lastReason = data.reason.trim();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (data.event === 'throttled') {
|
|
225
|
+
this._parallel.throttledCount++;
|
|
226
|
+
this._parallel.lastThrottledAt = new Date().toISOString();
|
|
227
|
+
} else if (data.event === 'recovered') {
|
|
228
|
+
this._parallel.recoveredCount++;
|
|
229
|
+
this._parallel.lastRecoveredAt = new Date().toISOString();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
128
233
|
/**
|
|
129
234
|
* Set the overall orchestration state.
|
|
130
235
|
*
|
|
@@ -219,6 +324,8 @@ class StatusMonitor {
|
|
|
219
324
|
currentBatch: this._currentBatch,
|
|
220
325
|
totalBatches: this._totalBatches,
|
|
221
326
|
specs,
|
|
327
|
+
rateLimit: { ...this._rateLimit },
|
|
328
|
+
parallel: { ...this._parallel },
|
|
222
329
|
};
|
|
223
330
|
}
|
|
224
331
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
const { createMoquiAdapterHandler } = require('./moqui-adapter');
|
|
2
|
+
|
|
3
|
+
const DEFAULT_READINESS_SUCCESS = {
|
|
2
4
|
passed: true,
|
|
3
5
|
reason: 'default-ready'
|
|
4
6
|
};
|
|
@@ -130,6 +132,12 @@ function normalizeReadinessResult(rawResult, fallbackName) {
|
|
|
130
132
|
|
|
131
133
|
class BindingRegistry {
|
|
132
134
|
constructor(options = {}) {
|
|
135
|
+
this.projectRoot = options.projectRoot || process.cwd();
|
|
136
|
+
this.moquiConfigPath = typeof options.moquiConfigPath === 'string' && options.moquiConfigPath.trim().length > 0
|
|
137
|
+
? options.moquiConfigPath.trim()
|
|
138
|
+
: undefined;
|
|
139
|
+
this.useMoquiAdapter = options.useMoquiAdapter !== false;
|
|
140
|
+
|
|
133
141
|
this.handlers = [];
|
|
134
142
|
this.fallbackHandler = {
|
|
135
143
|
id: 'builtin.default',
|
|
@@ -158,6 +166,14 @@ class BindingRegistry {
|
|
|
158
166
|
}
|
|
159
167
|
|
|
160
168
|
registerDefaultHandlers() {
|
|
169
|
+
if (this.useMoquiAdapter) {
|
|
170
|
+
this.register(createMoquiAdapterHandler({
|
|
171
|
+
projectRoot: this.projectRoot,
|
|
172
|
+
configPath: this.moquiConfigPath,
|
|
173
|
+
allowSpecErpFallback: true
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
|
|
161
177
|
this.register({
|
|
162
178
|
id: 'builtin.erp-sim',
|
|
163
179
|
match: { refPrefix: 'spec.erp.' },
|
|
@@ -12,6 +12,19 @@ const DEFAULT_CONFIG_FILENAME = 'moqui-adapter.json';
|
|
|
12
12
|
const ENTITY_OPERATIONS = ['list', 'get', 'create', 'update', 'delete'];
|
|
13
13
|
const SERVICE_OPERATIONS = ['invoke', 'async', 'job-status'];
|
|
14
14
|
|
|
15
|
+
function resolveAdapterConfigPath(configPath, projectRoot) {
|
|
16
|
+
if (configPath) {
|
|
17
|
+
return path.resolve(projectRoot || process.cwd(), configPath);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return path.resolve(projectRoot || process.cwd(), DEFAULT_CONFIG_FILENAME);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hasAdapterConfigFile(configPath, projectRoot) {
|
|
24
|
+
const resolvedPath = resolveAdapterConfigPath(configPath, projectRoot);
|
|
25
|
+
return fs.pathExistsSync(resolvedPath);
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
/**
|
|
16
29
|
* Load and validate adapter config from file.
|
|
17
30
|
* @param {string} [configPath] - Path to moqui-adapter.json
|
|
@@ -19,9 +32,7 @@ const SERVICE_OPERATIONS = ['invoke', 'async', 'job-status'];
|
|
|
19
32
|
* @returns {{ config: Object, error?: string }}
|
|
20
33
|
*/
|
|
21
34
|
function loadAdapterConfig(configPath, projectRoot) {
|
|
22
|
-
const resolvedPath = configPath
|
|
23
|
-
? path.resolve(projectRoot || process.cwd(), configPath)
|
|
24
|
-
: path.resolve(projectRoot || process.cwd(), DEFAULT_CONFIG_FILENAME);
|
|
35
|
+
const resolvedPath = resolveAdapterConfigPath(configPath, projectRoot);
|
|
25
36
|
|
|
26
37
|
let rawContent;
|
|
27
38
|
|
|
@@ -404,6 +415,8 @@ function buildHttpRequest(descriptor, payload = {}) {
|
|
|
404
415
|
*/
|
|
405
416
|
function createMoquiAdapterHandler(options = {}) {
|
|
406
417
|
const HANDLER_ID = 'moqui.adapter';
|
|
418
|
+
const allowSpecErpFallback = options.allowSpecErpFallback !== false;
|
|
419
|
+
const strictMatch = options.strictMatch === true;
|
|
407
420
|
|
|
408
421
|
let client = options.client || null;
|
|
409
422
|
let configLoaded = false;
|
|
@@ -441,12 +454,36 @@ function createMoquiAdapterHandler(options = {}) {
|
|
|
441
454
|
return { client };
|
|
442
455
|
}
|
|
443
456
|
|
|
457
|
+
function shouldHandleBindingRef(bindingRef) {
|
|
458
|
+
const ref = String(bindingRef || '').trim();
|
|
459
|
+
|
|
460
|
+
if (!ref) {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (ref.startsWith('moqui.')) {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (!ref.startsWith('spec.erp.')) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (strictMatch || !allowSpecErpFallback) {
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (client) {
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return hasAdapterConfigFile(options.configPath, options.projectRoot);
|
|
481
|
+
}
|
|
482
|
+
|
|
444
483
|
return {
|
|
445
484
|
id: HANDLER_ID,
|
|
446
485
|
|
|
447
|
-
match: {
|
|
448
|
-
refPrefix: ['spec.erp.', 'moqui.']
|
|
449
|
-
},
|
|
486
|
+
match: (node = {}) => shouldHandleBindingRef(node.binding_ref || node.ref),
|
|
450
487
|
|
|
451
488
|
/**
|
|
452
489
|
* Execute a binding node against the Moqui REST API.
|
|
@@ -571,6 +608,8 @@ module.exports = {
|
|
|
571
608
|
mapMoquiResponseToResult,
|
|
572
609
|
buildHttpRequest,
|
|
573
610
|
createMoquiAdapterHandler,
|
|
611
|
+
resolveAdapterConfigPath,
|
|
612
|
+
hasAdapterConfigFile,
|
|
574
613
|
// Exported for testing
|
|
575
614
|
applyConfigDefaults,
|
|
576
615
|
DEFAULT_TIMEOUT,
|
|
@@ -5,6 +5,7 @@ const { URL } = require('url');
|
|
|
5
5
|
const DEFAULT_TIMEOUT = 30000;
|
|
6
6
|
const DEFAULT_RETRY_COUNT = 2;
|
|
7
7
|
const DEFAULT_RETRY_DELAY = 1000;
|
|
8
|
+
const DEFAULT_MAX_RETRY_DELAY = 30000;
|
|
8
9
|
|
|
9
10
|
const RETRYABLE_NETWORK_ERRORS = [
|
|
10
11
|
'ECONNREFUSED',
|
|
@@ -26,7 +27,57 @@ function isRetryableNetworkError(error) {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
function isRetryableStatusCode(statusCode) {
|
|
29
|
-
return statusCode >= 500 && statusCode <= 599;
|
|
30
|
+
return statusCode === 429 || (statusCode >= 500 && statusCode <= 599);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function parseRetryAfterMs(headers = {}) {
|
|
34
|
+
if (!headers || typeof headers !== 'object') {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const retryAfterValue = headers['retry-after'] || headers['Retry-After'];
|
|
39
|
+
|
|
40
|
+
if (retryAfterValue === undefined || retryAfterValue === null) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const asNumber = Number(retryAfterValue);
|
|
45
|
+
if (Number.isFinite(asNumber) && asNumber >= 0) {
|
|
46
|
+
return Math.floor(asNumber * 1000);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const asDateMs = Date.parse(String(retryAfterValue));
|
|
50
|
+
if (!Number.isFinite(asDateMs)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return Math.max(0, asDateMs - Date.now());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function clampRetryDelay(ms, maxRetryDelay) {
|
|
58
|
+
const parsed = Number(ms);
|
|
59
|
+
|
|
60
|
+
if (!Number.isFinite(parsed)) {
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const bounded = Math.max(0, parsed);
|
|
65
|
+
if (!Number.isFinite(maxRetryDelay) || maxRetryDelay <= 0) {
|
|
66
|
+
return Math.floor(bounded);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return Math.floor(Math.min(maxRetryDelay, bounded));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function computeRetryDelayMs(attempt, baseRetryDelay, retryAfterMs, maxRetryDelay) {
|
|
73
|
+
if (Number.isFinite(retryAfterMs) && retryAfterMs >= 0) {
|
|
74
|
+
return clampRetryDelay(retryAfterMs, maxRetryDelay);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const exponent = Math.max(0, attempt);
|
|
78
|
+
const backoff = Number(baseRetryDelay) * Math.pow(2, exponent);
|
|
79
|
+
|
|
80
|
+
return clampRetryDelay(backoff, maxRetryDelay);
|
|
30
81
|
}
|
|
31
82
|
|
|
32
83
|
function delay(ms) {
|
|
@@ -66,7 +117,8 @@ class MoquiClient {
|
|
|
66
117
|
credentials: config.credentials || {},
|
|
67
118
|
timeout: typeof config.timeout === 'number' ? config.timeout : DEFAULT_TIMEOUT,
|
|
68
119
|
retryCount: typeof config.retryCount === 'number' ? config.retryCount : DEFAULT_RETRY_COUNT,
|
|
69
|
-
retryDelay: typeof config.retryDelay === 'number' ? config.retryDelay : DEFAULT_RETRY_DELAY
|
|
120
|
+
retryDelay: typeof config.retryDelay === 'number' ? config.retryDelay : DEFAULT_RETRY_DELAY,
|
|
121
|
+
maxRetryDelay: typeof config.maxRetryDelay === 'number' ? config.maxRetryDelay : DEFAULT_MAX_RETRY_DELAY
|
|
70
122
|
};
|
|
71
123
|
|
|
72
124
|
this.accessToken = null;
|
|
@@ -338,10 +390,6 @@ class MoquiClient {
|
|
|
338
390
|
const maxAttempts = this.config.retryCount + 1;
|
|
339
391
|
|
|
340
392
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
341
|
-
if (attempt > 0) {
|
|
342
|
-
await delay(this.config.retryDelay);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
393
|
try {
|
|
346
394
|
// Update auth header in case token was refreshed
|
|
347
395
|
if (this.accessToken) {
|
|
@@ -381,16 +429,36 @@ class MoquiClient {
|
|
|
381
429
|
};
|
|
382
430
|
}
|
|
383
431
|
|
|
384
|
-
// Retry on 5xx
|
|
432
|
+
// Retry on 429 and 5xx
|
|
385
433
|
if (isRetryableStatusCode(response.statusCode)) {
|
|
434
|
+
const isRateLimited = response.statusCode === 429;
|
|
435
|
+
|
|
386
436
|
lastError = {
|
|
387
437
|
success: false,
|
|
388
438
|
error: {
|
|
389
|
-
code: 'MOQUI_ERROR',
|
|
390
|
-
message:
|
|
439
|
+
code: isRateLimited ? 'RATE_LIMITED' : 'MOQUI_ERROR',
|
|
440
|
+
message: isRateLimited
|
|
441
|
+
? `Rate limited: ${response.statusCode}`
|
|
442
|
+
: `Server error: ${response.statusCode}`,
|
|
391
443
|
details: response.body
|
|
392
444
|
}
|
|
393
445
|
};
|
|
446
|
+
|
|
447
|
+
if (attempt >= maxAttempts - 1) {
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const retryDelayMs = computeRetryDelayMs(
|
|
452
|
+
attempt,
|
|
453
|
+
this.config.retryDelay,
|
|
454
|
+
parseRetryAfterMs(response.headers),
|
|
455
|
+
this.config.maxRetryDelay
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
if (retryDelayMs > 0) {
|
|
459
|
+
await delay(retryDelayMs);
|
|
460
|
+
}
|
|
461
|
+
|
|
394
462
|
continue;
|
|
395
463
|
}
|
|
396
464
|
|
|
@@ -415,6 +483,22 @@ class MoquiClient {
|
|
|
415
483
|
message: `Network error: ${error.message} (${fullUrl})`
|
|
416
484
|
}
|
|
417
485
|
};
|
|
486
|
+
|
|
487
|
+
if (attempt >= maxAttempts - 1) {
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const retryDelayMs = computeRetryDelayMs(
|
|
492
|
+
attempt,
|
|
493
|
+
this.config.retryDelay,
|
|
494
|
+
null,
|
|
495
|
+
this.config.maxRetryDelay
|
|
496
|
+
);
|
|
497
|
+
|
|
498
|
+
if (retryDelayMs > 0) {
|
|
499
|
+
await delay(retryDelayMs);
|
|
500
|
+
}
|
|
501
|
+
|
|
418
502
|
continue;
|
|
419
503
|
}
|
|
420
504
|
|
|
@@ -1131,44 +1131,15 @@ function analyzeResources(discovery, options = {}) {
|
|
|
1131
1131
|
return results;
|
|
1132
1132
|
}
|
|
1133
1133
|
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
/**
|
|
1137
|
-
* Generate a scene manifest object for a pattern match.
|
|
1138
|
-
* Produces a manifest with correct apiVersion, kind, bindings, model_scope,
|
|
1139
|
-
* and governance_contract based on the pattern type.
|
|
1140
|
-
*
|
|
1141
|
-
* Pattern rules for bindings:
|
|
1142
|
-
* - "crud": 5 bindings (list, get = query; create, update, delete = mutation with side_effect)
|
|
1143
|
-
* - "query": 2 bindings (list, get = query)
|
|
1144
|
-
* - "workflow": service invoke bindings (type: 'invoke', ref from bindingRefs)
|
|
1145
|
-
*
|
|
1146
|
-
* Governance rules:
|
|
1147
|
-
* - "query": risk_level "low", approval.required false, no idempotency
|
|
1148
|
-
* - "crud"/"workflow": risk_level "medium", approval.required true, idempotency.required true
|
|
1149
|
-
*
|
|
1150
|
-
* @param {PatternMatch} match - Matched pattern
|
|
1151
|
-
* @returns {Object|null} Scene manifest object, or null for invalid input
|
|
1152
|
-
*/
|
|
1153
|
-
function generateSceneManifest(match) {
|
|
1154
|
-
if (!match || !match.pattern || !match.primaryResource) {
|
|
1155
|
-
return null;
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1134
|
+
function buildBaseBindings(match) {
|
|
1158
1135
|
const pattern = match.pattern;
|
|
1159
|
-
const primaryResource = match.primaryResource;
|
|
1160
|
-
const packageName = derivePackageName(match);
|
|
1161
|
-
|
|
1162
|
-
// Build bindings based on pattern type
|
|
1163
1136
|
const bindings = [];
|
|
1164
1137
|
|
|
1165
1138
|
if (pattern === 'crud' || pattern === 'query') {
|
|
1166
|
-
// Entity-based patterns use bindingRefs from the match
|
|
1167
1139
|
const primaryEntity = Array.isArray(match.entities) && match.entities.length > 0
|
|
1168
1140
|
? match.entities[0]
|
|
1169
|
-
: primaryResource;
|
|
1141
|
+
: match.primaryResource;
|
|
1170
1142
|
|
|
1171
|
-
// Query bindings (list, get)
|
|
1172
1143
|
bindings.push({
|
|
1173
1144
|
type: 'query',
|
|
1174
1145
|
ref: `moqui.${primaryEntity}.list`,
|
|
@@ -1183,7 +1154,6 @@ function generateSceneManifest(match) {
|
|
|
1183
1154
|
});
|
|
1184
1155
|
|
|
1185
1156
|
if (pattern === 'crud') {
|
|
1186
|
-
// Mutation bindings (create, update, delete)
|
|
1187
1157
|
bindings.push({
|
|
1188
1158
|
type: 'mutation',
|
|
1189
1159
|
ref: `moqui.${primaryEntity}.create`,
|
|
@@ -1207,9 +1177,7 @@ function generateSceneManifest(match) {
|
|
|
1207
1177
|
});
|
|
1208
1178
|
}
|
|
1209
1179
|
} else if (pattern === 'workflow') {
|
|
1210
|
-
// Workflow patterns use service invoke bindings from bindingRefs
|
|
1211
1180
|
const refs = Array.isArray(match.bindingRefs) ? match.bindingRefs : [];
|
|
1212
|
-
|
|
1213
1181
|
for (const ref of refs) {
|
|
1214
1182
|
bindings.push({
|
|
1215
1183
|
type: 'invoke',
|
|
@@ -1220,37 +1188,324 @@ function generateSceneManifest(match) {
|
|
|
1220
1188
|
}
|
|
1221
1189
|
}
|
|
1222
1190
|
|
|
1223
|
-
|
|
1224
|
-
|
|
1191
|
+
return bindings;
|
|
1192
|
+
}
|
|
1225
1193
|
|
|
1226
|
-
|
|
1227
|
-
|
|
1194
|
+
function deriveBindingIntent(binding, primaryResource) {
|
|
1195
|
+
const ref = String(binding.ref || '');
|
|
1228
1196
|
|
|
1229
|
-
if (
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1197
|
+
if (ref.endsWith('.list')) {
|
|
1198
|
+
return `List ${primaryResource} records from Moqui`;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
if (ref.endsWith('.get')) {
|
|
1202
|
+
return `Retrieve a single ${primaryResource} record`;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
if (ref.endsWith('.create')) {
|
|
1206
|
+
return `Create a new ${primaryResource} record`;
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
if (ref.endsWith('.update')) {
|
|
1210
|
+
return `Update an existing ${primaryResource} record`;
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
if (ref.endsWith('.delete')) {
|
|
1214
|
+
return `Delete an existing ${primaryResource} record`;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
if (ref.endsWith('.invoke')) {
|
|
1218
|
+
return `Invoke workflow service for ${primaryResource}`;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
return `Execute ${ref}`;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function deriveBindingPreconditions(binding, previousRef) {
|
|
1225
|
+
const checks = ['Moqui adapter authentication is valid'];
|
|
1226
|
+
|
|
1227
|
+
if (binding.type === 'query') {
|
|
1228
|
+
checks.push('Read scope permits this query');
|
|
1229
|
+
} else if (binding.type === 'mutation') {
|
|
1230
|
+
checks.push('Input payload validation passed');
|
|
1231
|
+
} else if (binding.type === 'invoke') {
|
|
1232
|
+
checks.push('Workflow input contract is satisfied');
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
if (previousRef) {
|
|
1236
|
+
checks.push(`Dependency ${previousRef} completed successfully`);
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
return checks;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function deriveBindingPostconditions(binding) {
|
|
1243
|
+
if (binding.type === 'query') {
|
|
1244
|
+
return ['Query result is available for downstream composition'];
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
if (binding.type === 'mutation') {
|
|
1248
|
+
return ['Mutation result is captured and write scope is consistent'];
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
if (binding.type === 'invoke') {
|
|
1252
|
+
return ['Workflow step output is captured for downstream execution'];
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
return ['Binding execution result is available'];
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
function addBindingSemantics(baseBindings, primaryResource) {
|
|
1259
|
+
const bindings = [];
|
|
1260
|
+
|
|
1261
|
+
for (let i = 0; i < baseBindings.length; i++) {
|
|
1262
|
+
const base = baseBindings[i];
|
|
1263
|
+
const previous = i > 0 ? baseBindings[i - 1] : null;
|
|
1264
|
+
const binding = {
|
|
1265
|
+
...base,
|
|
1266
|
+
intent: deriveBindingIntent(base, primaryResource),
|
|
1267
|
+
preconditions: deriveBindingPreconditions(base, previous ? previous.ref : null),
|
|
1268
|
+
postconditions: deriveBindingPostconditions(base)
|
|
1235
1269
|
};
|
|
1270
|
+
|
|
1271
|
+
if (previous && previous.ref) {
|
|
1272
|
+
binding.depends_on = previous.ref;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
bindings.push(binding);
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
return bindings;
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function buildDataLineage(bindings, pattern, primaryResource) {
|
|
1282
|
+
if (!Array.isArray(bindings) || bindings.length === 0) {
|
|
1283
|
+
return {
|
|
1284
|
+
sources: [],
|
|
1285
|
+
transforms: [],
|
|
1286
|
+
sinks: []
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
const firstRef = bindings[0].ref;
|
|
1291
|
+
const lastRef = bindings[bindings.length - 1].ref;
|
|
1292
|
+
const sourceField = `${toKebabCase(primaryResource)}Id`;
|
|
1293
|
+
|
|
1294
|
+
const transforms = [
|
|
1295
|
+
{
|
|
1296
|
+
operation: 'normalizeInput',
|
|
1297
|
+
description: `Normalize ${primaryResource} request payload for template execution`
|
|
1298
|
+
}
|
|
1299
|
+
];
|
|
1300
|
+
|
|
1301
|
+
if (pattern === 'workflow') {
|
|
1302
|
+
transforms.push({
|
|
1303
|
+
operation: 'orchestrateWorkflow',
|
|
1304
|
+
description: `Coordinate service chain for ${primaryResource}`
|
|
1305
|
+
});
|
|
1306
|
+
} else if (pattern === 'crud') {
|
|
1307
|
+
transforms.push({
|
|
1308
|
+
operation: 'applyMutationGuard',
|
|
1309
|
+
description: `Apply mutation and idempotency guard for ${primaryResource}`
|
|
1310
|
+
});
|
|
1236
1311
|
} else {
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1312
|
+
transforms.push({
|
|
1313
|
+
operation: 'projectQueryResult',
|
|
1314
|
+
description: `Project query result set for ${primaryResource}`
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
return {
|
|
1319
|
+
sources: [
|
|
1320
|
+
{
|
|
1321
|
+
ref: firstRef,
|
|
1322
|
+
fields: [sourceField, 'statusId']
|
|
1323
|
+
}
|
|
1324
|
+
],
|
|
1325
|
+
transforms,
|
|
1326
|
+
sinks: [
|
|
1327
|
+
{
|
|
1328
|
+
ref: lastRef,
|
|
1329
|
+
fields: [sourceField, 'statusId']
|
|
1253
1330
|
}
|
|
1331
|
+
]
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
function buildEntityRefs(match, primaryResource) {
|
|
1336
|
+
const refs = Array.isArray(match.entities) && match.entities.length > 0
|
|
1337
|
+
? match.entities.filter(Boolean)
|
|
1338
|
+
: [primaryResource];
|
|
1339
|
+
|
|
1340
|
+
return refs.map((entity, index) => ({
|
|
1341
|
+
id: String(entity),
|
|
1342
|
+
type: index === 0 ? 'primary' : 'related'
|
|
1343
|
+
}));
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function buildEntityRelations(entityRefs) {
|
|
1347
|
+
if (!Array.isArray(entityRefs) || entityRefs.length === 0) {
|
|
1348
|
+
return [];
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
const relations = [];
|
|
1352
|
+
const primaryId = entityRefs[0].id;
|
|
1353
|
+
|
|
1354
|
+
for (let i = 1; i < entityRefs.length; i++) {
|
|
1355
|
+
relations.push({
|
|
1356
|
+
source: primaryId,
|
|
1357
|
+
target: entityRefs[i].id,
|
|
1358
|
+
type: 'composes'
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
if (relations.length === 0) {
|
|
1363
|
+
relations.push({
|
|
1364
|
+
source: primaryId,
|
|
1365
|
+
target: 'metadata_view',
|
|
1366
|
+
type: 'produces'
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
return relations;
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function buildBusinessRules(pattern, bindings, primaryResource) {
|
|
1374
|
+
const firstRef = bindings[0] ? bindings[0].ref : null;
|
|
1375
|
+
const lastRef = bindings[bindings.length - 1] ? bindings[bindings.length - 1].ref : null;
|
|
1376
|
+
|
|
1377
|
+
const rules = [
|
|
1378
|
+
{
|
|
1379
|
+
id: `rule.${toKebabCase(primaryResource)}.binding-order`,
|
|
1380
|
+
description: `Bindings for ${primaryResource} must execute in declared dependency order`,
|
|
1381
|
+
bind_to: firstRef,
|
|
1382
|
+
status: 'enforced'
|
|
1383
|
+
}
|
|
1384
|
+
];
|
|
1385
|
+
|
|
1386
|
+
if (pattern === 'query') {
|
|
1387
|
+
rules.push({
|
|
1388
|
+
id: `rule.${toKebabCase(primaryResource)}.read-only`,
|
|
1389
|
+
description: `${primaryResource} query template must remain side-effect free`,
|
|
1390
|
+
bind_to: lastRef,
|
|
1391
|
+
status: 'active'
|
|
1392
|
+
});
|
|
1393
|
+
} else {
|
|
1394
|
+
rules.push({
|
|
1395
|
+
id: `rule.${toKebabCase(primaryResource)}.approval-or-idempotency`,
|
|
1396
|
+
description: `${primaryResource} template must enforce approval or idempotency guard`,
|
|
1397
|
+
bind_to: lastRef,
|
|
1398
|
+
status: 'active'
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
return rules;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
function buildDecisionLogic(pattern, bindings, primaryResource) {
|
|
1406
|
+
const lastRef = bindings[bindings.length - 1] ? bindings[bindings.length - 1].ref : null;
|
|
1407
|
+
const riskDecision = pattern === 'query'
|
|
1408
|
+
? 'Use low-risk dry-run defaults for query execution'
|
|
1409
|
+
: 'Use guarded execution with approval and retry policies';
|
|
1410
|
+
|
|
1411
|
+
return [
|
|
1412
|
+
{
|
|
1413
|
+
id: `decision.${toKebabCase(primaryResource)}.risk-strategy`,
|
|
1414
|
+
description: riskDecision,
|
|
1415
|
+
bind_to: lastRef,
|
|
1416
|
+
status: 'resolved',
|
|
1417
|
+
automated: true
|
|
1418
|
+
},
|
|
1419
|
+
{
|
|
1420
|
+
id: `decision.${toKebabCase(primaryResource)}.retry-strategy`,
|
|
1421
|
+
description: 'Apply timeout/retry profile derived from template contract',
|
|
1422
|
+
bind_to: lastRef,
|
|
1423
|
+
status: 'resolved',
|
|
1424
|
+
automated: true
|
|
1425
|
+
}
|
|
1426
|
+
];
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
function buildAgentHints(pattern, primaryResource, bindings) {
|
|
1430
|
+
const complexity = pattern === 'query' ? 'low' : 'medium';
|
|
1431
|
+
const baseDuration = pattern === 'query' ? 1800 : 3000;
|
|
1432
|
+
const permissions = pattern === 'query'
|
|
1433
|
+
? ['moqui.read']
|
|
1434
|
+
: ['moqui.read', 'moqui.write'];
|
|
1435
|
+
|
|
1436
|
+
return {
|
|
1437
|
+
summary: `${pattern.toUpperCase()} template extracted for ${primaryResource} with Moqui-aware ontology`,
|
|
1438
|
+
complexity,
|
|
1439
|
+
estimated_duration_ms: baseDuration + (bindings.length * 150),
|
|
1440
|
+
required_permissions: permissions,
|
|
1441
|
+
suggested_sequence: bindings.map((binding) => binding.ref),
|
|
1442
|
+
rollback_strategy: pattern === 'query'
|
|
1443
|
+
? 'Re-run query with previous filters'
|
|
1444
|
+
: 'Reconcile idempotency key and rollback to pre-mutation snapshot'
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
// ─── Scene Manifest Generation ────────────────────────────────────
|
|
1449
|
+
|
|
1450
|
+
/**
|
|
1451
|
+
* Generate a scene manifest object for a pattern match.
|
|
1452
|
+
* Produces a manifest with correct apiVersion, kind, bindings, model_scope,
|
|
1453
|
+
* and governance_contract based on the pattern type.
|
|
1454
|
+
*
|
|
1455
|
+
* Pattern rules for bindings:
|
|
1456
|
+
* - "crud": 5 bindings (list, get = query; create, update, delete = mutation with side_effect)
|
|
1457
|
+
* - "query": 2 bindings (list, get = query)
|
|
1458
|
+
* - "workflow": service invoke bindings (type: 'invoke', ref from bindingRefs)
|
|
1459
|
+
*
|
|
1460
|
+
* Governance rules:
|
|
1461
|
+
* - "query": risk_level "low", approval.required false, no idempotency
|
|
1462
|
+
* - "crud"/"workflow": risk_level "medium", approval.required true, idempotency.required true
|
|
1463
|
+
*
|
|
1464
|
+
* @param {PatternMatch} match - Matched pattern
|
|
1465
|
+
* @returns {Object|null} Scene manifest object, or null for invalid input
|
|
1466
|
+
*/
|
|
1467
|
+
function generateSceneManifest(match) {
|
|
1468
|
+
if (!match || !match.pattern || !match.primaryResource) {
|
|
1469
|
+
return null;
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
const pattern = match.pattern;
|
|
1473
|
+
const primaryResource = match.primaryResource;
|
|
1474
|
+
const packageName = derivePackageName(match);
|
|
1475
|
+
const gov = match.governance || {};
|
|
1476
|
+
|
|
1477
|
+
const baseBindings = buildBaseBindings(match);
|
|
1478
|
+
const bindings = addBindingSemantics(baseBindings, primaryResource);
|
|
1479
|
+
|
|
1480
|
+
// Build model_scope from match
|
|
1481
|
+
const modelScope = match.modelScope || { read: [], write: [] };
|
|
1482
|
+
|
|
1483
|
+
// Build governance_contract based on pattern type
|
|
1484
|
+
const riskLevel = gov.riskLevel || (pattern === 'query' ? 'low' : 'medium');
|
|
1485
|
+
const approvalRequired = gov.approvalRequired !== undefined
|
|
1486
|
+
? gov.approvalRequired
|
|
1487
|
+
: (pattern !== 'query');
|
|
1488
|
+
const idempotencyRequired = gov.idempotencyRequired !== undefined
|
|
1489
|
+
? gov.idempotencyRequired
|
|
1490
|
+
: (pattern !== 'query');
|
|
1491
|
+
const idempotencyKey = gov.idempotencyKey || deriveIdempotencyKey(
|
|
1492
|
+
Array.isArray(match.entities) && match.entities.length > 0
|
|
1493
|
+
? match.entities[0]
|
|
1494
|
+
: primaryResource
|
|
1495
|
+
);
|
|
1496
|
+
|
|
1497
|
+
const governanceContract = {
|
|
1498
|
+
risk_level: riskLevel,
|
|
1499
|
+
approval: {
|
|
1500
|
+
required: approvalRequired
|
|
1501
|
+
},
|
|
1502
|
+
data_lineage: buildDataLineage(bindings, pattern, primaryResource)
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
if (idempotencyRequired) {
|
|
1506
|
+
governanceContract.idempotency = {
|
|
1507
|
+
required: true,
|
|
1508
|
+
key: idempotencyKey
|
|
1254
1509
|
};
|
|
1255
1510
|
}
|
|
1256
1511
|
|
|
@@ -1311,6 +1566,41 @@ function generatePackageContract(match) {
|
|
|
1311
1566
|
const primaryResource = match.primaryResource;
|
|
1312
1567
|
const packageName = derivePackageName(match);
|
|
1313
1568
|
const gov = match.governance || {};
|
|
1569
|
+
const baseBindings = buildBaseBindings(match);
|
|
1570
|
+
const bindings = addBindingSemantics(baseBindings, primaryResource);
|
|
1571
|
+
const riskLevel = gov.riskLevel || (pattern === 'query' ? 'low' : 'medium');
|
|
1572
|
+
const approvalRequired = gov.approvalRequired !== undefined
|
|
1573
|
+
? gov.approvalRequired
|
|
1574
|
+
: (pattern !== 'query');
|
|
1575
|
+
const idempotencyRequired = gov.idempotencyRequired !== undefined
|
|
1576
|
+
? gov.idempotencyRequired
|
|
1577
|
+
: (pattern !== 'query');
|
|
1578
|
+
const idempotencyKey = gov.idempotencyKey || deriveIdempotencyKey(
|
|
1579
|
+
Array.isArray(match.entities) && match.entities.length > 0
|
|
1580
|
+
? match.entities[0]
|
|
1581
|
+
: primaryResource
|
|
1582
|
+
);
|
|
1583
|
+
const entityRefs = buildEntityRefs(match, primaryResource);
|
|
1584
|
+
const relations = buildEntityRelations(entityRefs);
|
|
1585
|
+
const hasMetadataView = entityRefs.some((entity) => entity.id === 'metadata_view');
|
|
1586
|
+
|
|
1587
|
+
if (!hasMetadataView) {
|
|
1588
|
+
entityRefs.push({ id: 'metadata_view', type: 'projection' });
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
const hasMetadataRelation = relations.some((relation) => (
|
|
1592
|
+
relation.source === entityRefs[0].id
|
|
1593
|
+
&& relation.target === 'metadata_view'
|
|
1594
|
+
&& relation.type === 'produces'
|
|
1595
|
+
));
|
|
1596
|
+
|
|
1597
|
+
if (!hasMetadataRelation) {
|
|
1598
|
+
relations.push({
|
|
1599
|
+
source: entityRefs[0].id,
|
|
1600
|
+
target: 'metadata_view',
|
|
1601
|
+
type: 'produces'
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1314
1604
|
|
|
1315
1605
|
// Derive summary based on pattern type
|
|
1316
1606
|
let summary;
|
|
@@ -1325,11 +1615,22 @@ function generatePackageContract(match) {
|
|
|
1325
1615
|
summary = `Template for ${primaryResource} extracted from Moqui ERP`;
|
|
1326
1616
|
}
|
|
1327
1617
|
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1618
|
+
const governanceContract = {
|
|
1619
|
+
risk_level: riskLevel,
|
|
1620
|
+
approval: {
|
|
1621
|
+
required: approvalRequired
|
|
1622
|
+
},
|
|
1623
|
+
data_lineage: buildDataLineage(bindings, pattern, primaryResource),
|
|
1624
|
+
business_rules: buildBusinessRules(pattern, bindings, primaryResource),
|
|
1625
|
+
decision_logic: buildDecisionLogic(pattern, bindings, primaryResource)
|
|
1626
|
+
};
|
|
1627
|
+
|
|
1628
|
+
if (idempotencyRequired) {
|
|
1629
|
+
governanceContract.idempotency = {
|
|
1630
|
+
required: true,
|
|
1631
|
+
key: idempotencyKey
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1333
1634
|
|
|
1334
1635
|
return {
|
|
1335
1636
|
apiVersion: PACKAGE_API_VERSION,
|
|
@@ -1338,7 +1639,8 @@ function generatePackageContract(match) {
|
|
|
1338
1639
|
group: 'kse.scene',
|
|
1339
1640
|
name: packageName,
|
|
1340
1641
|
version: '0.1.0',
|
|
1341
|
-
summary
|
|
1642
|
+
summary,
|
|
1643
|
+
description: `${summary}. Includes ontology graph hints, lineage tracing, and AI execution metadata.`
|
|
1342
1644
|
},
|
|
1343
1645
|
compatibility: {
|
|
1344
1646
|
kse_version: '>=1.39.0',
|
|
@@ -1367,8 +1669,24 @@ function generatePackageContract(match) {
|
|
|
1367
1669
|
governance: {
|
|
1368
1670
|
risk_level: riskLevel,
|
|
1369
1671
|
approval_required: approvalRequired,
|
|
1672
|
+
approval: {
|
|
1673
|
+
required: approvalRequired
|
|
1674
|
+
},
|
|
1675
|
+
idempotency: {
|
|
1676
|
+
required: idempotencyRequired,
|
|
1677
|
+
key: idempotencyKey
|
|
1678
|
+
},
|
|
1370
1679
|
rollback_supported: true
|
|
1371
|
-
}
|
|
1680
|
+
},
|
|
1681
|
+
capability_contract: {
|
|
1682
|
+
bindings
|
|
1683
|
+
},
|
|
1684
|
+
governance_contract: governanceContract,
|
|
1685
|
+
ontology_model: {
|
|
1686
|
+
entities: entityRefs,
|
|
1687
|
+
relations
|
|
1688
|
+
},
|
|
1689
|
+
agent_hints: buildAgentHints(pattern, primaryResource, bindings)
|
|
1372
1690
|
};
|
|
1373
1691
|
}
|
|
1374
1692
|
|
|
@@ -15,7 +15,11 @@ class RuntimeExecutor {
|
|
|
15
15
|
this.policyGate = options.policyGate || new PolicyGate();
|
|
16
16
|
this.auditEmitter = options.auditEmitter || new AuditEmitter(options.projectRoot || process.cwd(), options.audit || {});
|
|
17
17
|
this.evalBridge = options.evalBridge || new EvalBridge();
|
|
18
|
-
this.bindingRegistry = options.bindingRegistry || new BindingRegistry(
|
|
18
|
+
this.bindingRegistry = options.bindingRegistry || new BindingRegistry({
|
|
19
|
+
projectRoot: options.projectRoot || process.cwd(),
|
|
20
|
+
moquiConfigPath: options.moquiConfigPath,
|
|
21
|
+
useMoquiAdapter: options.useMoquiAdapter
|
|
22
|
+
});
|
|
19
23
|
this.bindingExecutor = typeof options.bindingExecutor === 'function' ? options.bindingExecutor : null;
|
|
20
24
|
this.adapterReadinessChecker = options.adapterReadinessChecker || this.defaultAdapterReadinessChecker.bind(this);
|
|
21
25
|
this.bindingPluginLoad = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kiro-spec-engine",
|
|
3
|
-
"version": "1.47.
|
|
3
|
+
"version": "1.47.15",
|
|
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": {
|