scene-capability-engine 3.6.55 → 3.6.57

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.
Files changed (32) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +7 -2
  3. package/README.zh.md +7 -2
  4. package/bin/scene-capability-engine.js +10 -0
  5. package/docs/app-intent-apply-contract.md +263 -0
  6. package/docs/autonomous-control-guide.md +12 -0
  7. package/docs/command-reference.md +15 -0
  8. package/docs/examples/app-intent-phase1/.sce/app/collections/planning-workbench.json +34 -0
  9. package/docs/examples/app-intent-phase1/.sce/app/collections/sales-workbench.json +40 -0
  10. package/docs/examples/app-intent-phase1/.sce/app/collections/warehouse-kiosk.json +35 -0
  11. package/docs/examples/app-intent-phase1/.sce/app/scene-profiles/planning-desktop.json +23 -0
  12. package/docs/examples/app-intent-phase1/.sce/app/scene-profiles/sales-desktop.json +24 -0
  13. package/docs/examples/app-intent-phase1/.sce/app/scene-profiles/warehouse-tablet.json +23 -0
  14. package/docs/examples/app-intent-phase1/README.md +29 -0
  15. package/docs/magicball-app-collection-phase-1.md +37 -0
  16. package/docs/magicball-cli-invocation-examples.md +1 -0
  17. package/docs/magicball-integration-doc-index.md +13 -6
  18. package/docs/magicball-sce-adaptation-guide.md +2 -0
  19. package/docs/releases/README.md +2 -0
  20. package/docs/releases/v3.6.56.md +19 -0
  21. package/docs/releases/v3.6.57.md +19 -0
  22. package/docs/spec-workflow.md +42 -0
  23. package/docs/zh/releases/README.md +2 -0
  24. package/docs/zh/releases/v3.6.56.md +19 -0
  25. package/docs/zh/releases/v3.6.57.md +19 -0
  26. package/lib/commands/spec-gate.js +57 -6
  27. package/lib/commands/spec-pipeline.js +13 -4
  28. package/lib/commands/spec-strategy.js +73 -0
  29. package/lib/problem/project-problem-projection.js +43 -0
  30. package/lib/spec/complexity-strategy.js +636 -0
  31. package/package.json +1 -1
  32. package/template/.sce/README.md +2 -2
@@ -84,6 +84,22 @@ Recommended split:
84
84
 
85
85
  This boundary matters because SQLite is suitable for local query-heavy facts, but it should not become the only source of truth for portable shared intent.
86
86
 
87
+ ## Copy-ready examples
88
+
89
+ Sample assets are available here:
90
+
91
+ - `docs/examples/app-intent-phase1/.sce/app/collections/*.json`
92
+ - `docs/examples/app-intent-phase1/.sce/app/scene-profiles/*.json`
93
+ - `docs/examples/app-intent-phase1/README.md`
94
+
95
+ These examples are intentionally realistic but still generic:
96
+
97
+ - sales desktop workspace
98
+ - planning desktop workspace
99
+ - warehouse tablet workspace
100
+
101
+ Use them as a starting point, then replace app keys and business metadata with project-specific values.
102
+
87
103
  ## Recommended phase-1 command shape
88
104
 
89
105
  Phase-1 overall command surface:
@@ -108,6 +124,10 @@ Execution rule:
108
124
  - local override updates now also have an explicit CLI governance surface instead of requiring manual file edits
109
125
  - execution should reuse the existing app runtime path
110
126
 
127
+ JSON response contract:
128
+
129
+ - `docs/app-intent-apply-contract.md`
130
+
111
131
  ## Frontend implication for MagicBall
112
132
 
113
133
  MagicBall should keep using current per-app runtime controls until this phase ships.
@@ -128,6 +148,23 @@ Once phase-1 lands, the preferred top-level interaction should become:
128
148
  3. explicitly apply the plan
129
149
  4. continue using per-app runtime controls for detail views
130
150
 
151
+ ## Phase 2 direction
152
+
153
+ Phase-2 should be planned as lightweight user-intent sync, not as device-state sync.
154
+
155
+ Recommended boundary:
156
+
157
+ - sync shared `app_collection` / `scene_profile` selections or bindings
158
+ - do not sync `device_installation` facts as the cross-device source of truth
159
+ - keep `device_override` local unless a project explicitly introduces a higher-level policy mechanism
160
+
161
+ Recommended deliverables for phase-2 planning:
162
+
163
+ 1. user binding model for scene/profile selection
164
+ 2. device-aware resolution order for `user intent + local override + capability tags`
165
+ 3. pull/push sync semantics with conflict visibility rather than silent overwrite
166
+ 4. explicit non-goal: do not force all devices into the same installed app set
167
+
131
168
  ## Practical conclusion
132
169
 
133
170
  SCE should support this direction, but it should do so by extending the current app/runtime model rather than replacing it.
@@ -44,6 +44,7 @@ Optional local override input:
44
44
  - `.sce/state/device/device-override.json`
45
45
  - use this for per-device add/remove exceptions instead of mutating shared collection/workspace definitions
46
46
  - update it explicitly via `sce device override upsert --input <json> --json`
47
+ - copy-ready collection/workspace examples live under `docs/examples/app-intent-phase1/.sce/app/...`
47
48
 
48
49
  Example local override patch:
49
50
  ```json
@@ -15,6 +15,7 @@ It is a navigation layer, not a new source of truth.
15
15
  These are the current first-line integration documents:
16
16
  - `docs/magicball-sce-adaptation-guide.md`
17
17
  - `docs/magicball-app-collection-phase-1.md`
18
+ - `docs/app-intent-apply-contract.md`
18
19
  - `docs/magicball-ui-surface-checklist.md`
19
20
  - `docs/magicball-mode-home-and-ontology-empty-state-playbook.md`
20
21
  - `docs/magicball-frontend-state-and-command-mapping.md`
@@ -37,28 +38,32 @@ These are the current first-line integration documents:
37
38
  - current device / collection / workspace / override baseline
38
39
  - shipped phase-1 install orchestration contract
39
40
 
40
- 4. `docs/magicball-mode-home-and-ontology-empty-state-playbook.md`
41
+ 4. `docs/app-intent-apply-contract.md`
42
+ - stable JSON response contract for `collection apply` and `scene workspace apply`
43
+ - execution/blocking/preflight semantics
44
+
45
+ 5. `docs/magicball-mode-home-and-ontology-empty-state-playbook.md`
41
46
  - two active frontend-sensitive defaults
42
47
  - serialized mode-home loading
43
48
  - ontology empty-state and starter-seed behavior
44
49
 
45
- 5. `docs/magicball-frontend-state-and-command-mapping.md`
50
+ 6. `docs/magicball-frontend-state-and-command-mapping.md`
46
51
  - page state ownership
47
52
  - command-to-action mapping
48
53
  - error and retry boundaries
49
54
 
50
- 6. `docs/magicball-write-auth-adaptation-guide.md`
55
+ 7. `docs/magicball-write-auth-adaptation-guide.md`
51
56
  - write authorization and lease handling
52
57
 
53
- 7. `docs/magicball-task-feedback-timeline-guide.md`
58
+ 8. `docs/magicball-task-feedback-timeline-guide.md`
54
59
  - task feedback cards
55
60
  - timeline view integration
56
61
 
57
- 8. `docs/magicball-cli-invocation-examples.md`
62
+ 9. `docs/magicball-cli-invocation-examples.md`
58
63
  - copy-ready CLI examples
59
64
  - wrapper and smoke verification patterns
60
65
 
61
- 9. `docs/magicball-integration-issue-tracker.md`
66
+ 10. `docs/magicball-integration-issue-tracker.md`
62
67
  - current cross-project truth
63
68
  - only open issues, active decisions, and verified resolutions
64
69
 
@@ -68,6 +73,7 @@ These are the current first-line integration documents:
68
73
  Use:
69
74
  - `docs/magicball-sce-adaptation-guide.md`
70
75
  - `docs/magicball-app-collection-phase-1.md`
76
+ - `docs/app-intent-apply-contract.md`
71
77
  - `docs/magicball-mode-home-and-ontology-empty-state-playbook.md`
72
78
  - `docs/magicball-frontend-state-and-command-mapping.md`
73
79
 
@@ -102,6 +108,7 @@ Use:
102
108
  | --- | --- | --- |
103
109
  | `magicball-sce-adaptation-guide.md` | main overview | low |
104
110
  | `magicball-app-collection-phase-1.md` | next-phase install model | medium |
111
+ | `app-intent-apply-contract.md` | apply JSON contract | medium |
105
112
  | `magicball-adaptation-task-checklist-v1.md` | execution checklist | medium |
106
113
  | `magicball-mode-home-and-ontology-empty-state-playbook.md` | frontend behavior policy | medium |
107
114
  | `magicball-frontend-state-and-command-mapping.md` | frontend implementation mapping | medium |
@@ -13,6 +13,7 @@ Implementation detail should live in the specialized documents listed below.
13
13
  Use these documents together:
14
14
  - `docs/magicball-integration-doc-index.md`
15
15
  - `docs/magicball-app-collection-phase-1.md`
16
+ - `docs/app-intent-apply-contract.md`
16
17
  - `docs/magicball-ui-surface-checklist.md`
17
18
  - `docs/magicball-mode-home-and-ontology-empty-state-playbook.md`
18
19
  - `docs/magicball-frontend-state-and-command-mapping.md`
@@ -58,6 +59,7 @@ Planned phase-1 position:
58
59
 
59
60
  Reference:
60
61
  - `docs/magicball-app-collection-phase-1.md`
62
+ - `docs/app-intent-apply-contract.md`
61
63
 
62
64
  ## Core Integration Positioning
63
65
 
@@ -9,6 +9,8 @@ This directory stores release-facing documents:
9
9
  ## Archived Versions
10
10
 
11
11
  - [Release checklist](../release-checklist.md)
12
+ - [v3.6.57 release notes](./v3.6.57.md)
13
+ - [v3.6.56 release notes](./v3.6.56.md)
12
14
  - [v3.6.55 release notes](./v3.6.55.md)
13
15
  - [v3.6.54 release notes](./v3.6.54.md)
14
16
  - [v3.6.48 release notes](./v3.6.48.md)
@@ -0,0 +1,19 @@
1
+ # v3.6.56 Release Notes
2
+
3
+ Release date: 2026-03-17
4
+
5
+ ## Highlights
6
+
7
+ - Added read-only `sce spec strategy assess` to decide whether a problem should stay `single-spec` or escalate to `multi-spec-program` / `research-program`.
8
+ - Added a formal strategy Spec for complex-problem program escalation so SCE can stop forcing highly entangled work through one oversized Spec.
9
+ - Added copy-ready phase-1 app intent examples plus a stable `apply` JSON contract document so scene-oriented install management can be adopted directly instead of inferred from source code.
10
+
11
+ ## Validation
12
+
13
+ - `npx jest tests/unit/commands/spec-strategy.test.js --runInBand`
14
+ - `node bin/sce.js --skip-steering-check --no-version-check spec strategy assess --goal "fix login button alignment in application home page" --json`
15
+ - `node scripts/release-doc-version-audit.js --fail-on-error`
16
+
17
+ ## Release Notes
18
+
19
+ - This patch turns SCE's existing multi-Spec and close-loop-program runtime into a better governed path for complex work. The key addition is not another executor, but an explicit strategy assessment layer that can say when one Spec is still enough and when the problem should first be treated as a coordinated program or a research-first clarification portfolio.
@@ -0,0 +1,19 @@
1
+ # v3.6.57 Release Notes
2
+
3
+ Release date: 2026-03-17
4
+
5
+ ## Highlights
6
+
7
+ - Added a read-only `strategy_assessment` block to single-Spec `sce spec gate run` and `sce spec pipeline run` output.
8
+ - Human-readable gate and pipeline flows now explicitly warn when a problem no longer fits one Spec and should escalate to `multi-spec-program` or `research-program`.
9
+ - Kept the integration conservative: SCE now surfaces the strategy mismatch early, but it does not auto-reroute execution or silently create more Specs.
10
+ - Fixed shared project problem projection sync to be idempotent when payload content is unchanged, preventing release/push governance from dirtying the worktree with timestamp-only rewrites.
11
+
12
+ ## Validation
13
+
14
+ - `npx jest tests/unit/commands/spec-gate.test.js tests/unit/commands/spec-pipeline.test.js --runInBand`
15
+ - `node scripts/release-doc-version-audit.js --fail-on-error`
16
+
17
+ ## Release Notes
18
+
19
+ - This patch closes the last obvious gap in the complex-problem strategy line added in `v3.6.56`. SCE could already assess whether a goal or Spec was too broad for one Spec; now the same judgment is visible inside the normal single-Spec execution path, so teams get an explicit warning before continuing blind decomposition in the wrong container. It also fixes a co-work publication edge case where project-shared problem projection refresh could dirty the repository even when nothing meaningful changed.
@@ -115,6 +115,48 @@ sce spec domain validate --spec <spec-id> --fail-on-gap --json
115
115
 
116
116
  This is the default way to avoid wrong-direction implementation and to force evidence-based correction loops.
117
117
 
118
+ ## When One Spec Is Not Enough
119
+
120
+ Not every problem should be forced into a single Spec.
121
+
122
+ Use this rule of thumb:
123
+
124
+ - simple problem: stay with one Spec
125
+ - medium complexity: split into a few implementation Specs and use orchestrate mode
126
+ - very complex problem: first treat it as a program, not as a single implementation Spec
127
+
128
+ Typical escalation signals:
129
+
130
+ - one Spec keeps expanding but still cannot produce stable executable tasks
131
+ - the problem spans multiple scenes, roles, or bounded contexts
132
+ - API/data contracts are still unclear enough that task breakdown would be guesswork
133
+ - business rules and policy boundaries are not yet settled
134
+ - a single problem-domain chain already shows multiple semi-independent clusters
135
+
136
+ Current SCE execution paths you can already use:
137
+
138
+ ```bash
139
+ # read-only strategy assessment
140
+ sce spec strategy assess --goal "broad complex goal" --json
141
+
142
+ # multi-spec implementation program
143
+ sce spec bootstrap --specs "spec-a,spec-b" --max-parallel 3
144
+
145
+ # broad goal -> autonomous master/sub program decomposition
146
+ sce auto close-loop-program "broad complex goal" --program-goals 4 --json
147
+ ```
148
+
149
+ `sce spec gate run --spec <spec-id>` and `sce spec pipeline run --spec <spec-id>` now both carry a read-only strategy advisory for single-Spec runs. If the current Spec is assessed as `multi-spec-program` or `research-program`, SCE surfaces that explicitly instead of silently pushing more decomposition into the same Spec.
150
+
151
+ The strategic gap currently being formalized is:
152
+
153
+ - how SCE should assess this threshold explicitly
154
+ - when it should recommend `single-spec` vs `multi-spec-program` vs `research-program`
155
+
156
+ Reference Spec:
157
+
158
+ - `.sce/specs/129-00-complex-problem-program-escalation/`
159
+
118
160
  ---
119
161
 
120
162
  ## Stage 1: Requirements
@@ -9,6 +9,8 @@
9
9
  ## 历史版本归档
10
10
 
11
11
  - [发布检查清单](../release-checklist.md)
12
+ - [v3.6.57 发布说明](./v3.6.57.md)
13
+ - [v3.6.56 发布说明](./v3.6.56.md)
12
14
  - [v3.6.55 发布说明](./v3.6.55.md)
13
15
  - [v3.6.54 发布说明](./v3.6.54.md)
14
16
  - [v3.6.48 发布说明](./v3.6.48.md)
@@ -0,0 +1,19 @@
1
+ # v3.6.56 发布说明
2
+
3
+ 发布日期:2026-03-17
4
+
5
+ ## 重点变化
6
+
7
+ - 新增只读命令 `sce spec strategy assess`,用于判断问题应继续使用 `single-spec`,还是升级为 `multi-spec-program` / `research-program`。
8
+ - 新增复杂问题升级策略 Spec,使 SCE 不再默认把高度纠缠的问题硬塞进一个超大 Spec。
9
+ - 补充 phase-1 应用意图样例与稳定的 `apply` JSON 契约文档,让面向场景的安装管理能力可以直接落地,而不是只能靠读源码理解。
10
+
11
+ ## 验证
12
+
13
+ - `npx jest tests/unit/commands/spec-strategy.test.js --runInBand`
14
+ - `node bin/sce.js --skip-steering-check --no-version-check spec strategy assess --goal "fix login button alignment in application home page" --json`
15
+ - `node scripts/release-doc-version-audit.js --fail-on-error`
16
+
17
+ ## 发布说明
18
+
19
+ - 这个补丁版的重点不是再造一个执行器,而是给 SCE 已有的多 Spec / close-loop-program 执行能力补上策略层。现在 SCE 可以更明确地判断:一个问题是否还适合留在单 Spec,还是应该先升级为协调型 program,或者先进入 research-first 的澄清拆分路径。
@@ -0,0 +1,19 @@
1
+ # v3.6.57 发布说明
2
+
3
+ 发布日期:2026-03-17
4
+
5
+ ## 重点变化
6
+
7
+ - 为单 spec 的 `sce spec gate run` 和 `sce spec pipeline run` 输出增加只读 `strategy_assessment` 字段。
8
+ - 当问题已经不适合继续塞进一个 spec,而应升级为 `multi-spec-program` 或 `research-program` 时,gate 和 pipeline 的人类可读输出会明确给出提示。
9
+ - 这次集成保持保守:SCE 会尽早暴露策略不匹配,但不会擅自改道执行,也不会静默创建更多 spec。
10
+ - 修复共享问题投影同步的幂等性;当内容未变化时,不再只因 `generated_at` 刷新就把仓库重新写脏,避免 push / 发版治理被时间戳漂移误伤。
11
+
12
+ ## 验证
13
+
14
+ - `npx jest tests/unit/commands/spec-gate.test.js tests/unit/commands/spec-pipeline.test.js --runInBand`
15
+ - `node scripts/release-doc-version-audit.js --fail-on-error`
16
+
17
+ ## 发布说明
18
+
19
+ - 这个补丁版把 `v3.6.56` 刚加上的复杂问题策略评估真正接入到了日常单 spec 主路径。现在 SCE 不只是“能评估”,而是会在正常的 gate / pipeline 过程中显式提醒:当前问题已经不该继续在一个 spec 里盲拆,而应升级为更合适的 program 路径。同时也修掉了 co-work 发布链路上的一个实际阻断点:共享问题投影在内容不变时不应因为时间戳刷新而制造脏工作区。
@@ -15,9 +15,11 @@ const { ResultEmitter } = require('../spec-gate/result-emitter');
15
15
  const { SessionStore } = require('../runtime/session-store');
16
16
  const { resolveSpecSceneBinding } = require('../runtime/scene-session-binding');
17
17
  const { bindMultiSpecSceneSession } = require('../runtime/multi-spec-scene-session');
18
+ const { assessComplexityStrategy } = require('../spec/complexity-strategy');
18
19
 
19
20
  async function runSpecGate(options = {}, dependencies = {}) {
20
21
  const projectPath = dependencies.projectPath || process.cwd();
22
+ const fileSystem = dependencies.fileSystem || fs;
21
23
  const sessionStore = dependencies.sessionStore || new SessionStore(projectPath);
22
24
  const specTargets = parseSpecTargets(options);
23
25
 
@@ -43,7 +45,7 @@ async function runSpecGate(options = {}, dependencies = {}) {
43
45
  })
44
46
  }, {
45
47
  projectPath,
46
- fileSystem: dependencies.fileSystem || fs,
48
+ fileSystem,
47
49
  sessionStore
48
50
  });
49
51
  }
@@ -51,7 +53,7 @@ async function runSpecGate(options = {}, dependencies = {}) {
51
53
  const specId = specTargets[0];
52
54
 
53
55
  const specPath = path.join(projectPath, '.sce', 'specs', specId);
54
- if (!await fs.pathExists(specPath)) {
56
+ if (!await fileSystem.pathExists(specPath)) {
55
57
  throw new Error(`Spec not found: ${specId}`);
56
58
  }
57
59
 
@@ -60,7 +62,7 @@ async function runSpecGate(options = {}, dependencies = {}) {
60
62
  allowNoScene: false
61
63
  }, {
62
64
  projectPath,
63
- fileSystem: dependencies.fileSystem || fs,
65
+ fileSystem,
64
66
  sessionStore
65
67
  });
66
68
  const linked = await sessionStore.startSpecSession({
@@ -83,13 +85,22 @@ async function runSpecGate(options = {}, dependencies = {}) {
83
85
  policy
84
86
  });
85
87
 
86
- const result = await engine.evaluate({ specId });
88
+ const gateResult = await engine.evaluate({ specId });
89
+ const result = {
90
+ ...gateResult,
91
+ strategy_assessment: await buildStrategyAssessment(specId, {
92
+ projectPath,
93
+ fileSystem,
94
+ strategyAssessor: dependencies.strategyAssessor
95
+ })
96
+ };
87
97
  const emitter = dependencies.emitter || new ResultEmitter(projectPath);
88
98
  const emitted = await emitter.emit(result, {
89
99
  json: options.json,
90
100
  out: options.out,
91
101
  silent: options.silent
92
102
  });
103
+ emitStrategyAdvisory(result.strategy_assessment, options);
93
104
 
94
105
  const decisionStatus = result.decision === 'no-go' ? 'failed' : 'completed';
95
106
  await sessionStore.completeSpecSession({
@@ -101,7 +112,8 @@ async function runSpecGate(options = {}, dependencies = {}) {
101
112
  spec: specId,
102
113
  decision: result.decision,
103
114
  score: result.score,
104
- report_path: emitted.outputPath || null
115
+ report_path: emitted.outputPath || null,
116
+ strategy_decision: result.strategy_assessment ? result.strategy_assessment.decision : null
105
117
  }
106
118
  });
107
119
 
@@ -132,6 +144,43 @@ async function runSpecGate(options = {}, dependencies = {}) {
132
144
  }
133
145
  }
134
146
 
147
+ async function buildStrategyAssessment(specId, dependencies = {}) {
148
+ const assess = dependencies.strategyAssessor || assessComplexityStrategy;
149
+ try {
150
+ return await assess({
151
+ spec: specId
152
+ }, {
153
+ projectPath: dependencies.projectPath,
154
+ fileSystem: dependencies.fileSystem
155
+ });
156
+ } catch (error) {
157
+ return {
158
+ decision: 'assessment-unavailable',
159
+ decision_reason: error.message,
160
+ advisory_only: true
161
+ };
162
+ }
163
+ }
164
+
165
+ function emitStrategyAdvisory(strategyAssessment, options = {}) {
166
+ if (!strategyAssessment || options.json || options.silent) {
167
+ return;
168
+ }
169
+
170
+ if (!['multi-spec-program', 'research-program'].includes(strategyAssessment.decision)) {
171
+ return;
172
+ }
173
+
174
+ console.log();
175
+ console.log(chalk.yellow('⚠ Strategy Advisory'));
176
+ console.log(` ${strategyAssessment.decision}: ${strategyAssessment.decision_reason}`);
177
+ if (Array.isArray(strategyAssessment.next_actions)) {
178
+ strategyAssessment.next_actions.forEach(action => {
179
+ console.log(` - ${action}`);
180
+ });
181
+ }
182
+ }
183
+
135
184
  async function generateSpecGatePolicyTemplate(options = {}, dependencies = {}) {
136
185
  const projectPath = dependencies.projectPath || process.cwd();
137
186
  const loader = dependencies.policyLoader || new PolicyLoader(projectPath);
@@ -220,5 +269,7 @@ module.exports = {
220
269
  registerSpecGateCommand,
221
270
  runSpecGate,
222
271
  generateSpecGatePolicyTemplate,
223
- _parseSpecTargets
272
+ _parseSpecTargets,
273
+ buildStrategyAssessment,
274
+ emitStrategyAdvisory
224
275
  };
@@ -13,9 +13,11 @@ const { createDefaultStageAdapters } = require('../spec/pipeline/stage-adapters'
13
13
  const { SessionStore } = require('../runtime/session-store');
14
14
  const { resolveSpecSceneBinding } = require('../runtime/scene-session-binding');
15
15
  const { bindMultiSpecSceneSession } = require('../runtime/multi-spec-scene-session');
16
+ const { buildStrategyAssessment, emitStrategyAdvisory } = require('./spec-gate');
16
17
 
17
18
  async function runSpecPipeline(options = {}, dependencies = {}) {
18
19
  const projectPath = dependencies.projectPath || process.cwd();
20
+ const fileSystem = dependencies.fileSystem || fs;
19
21
  const sessionStore = dependencies.sessionStore || new SessionStore(projectPath);
20
22
  const specTargets = parseSpecTargets(options);
21
23
  if (specTargets.length === 0) {
@@ -40,7 +42,7 @@ async function runSpecPipeline(options = {}, dependencies = {}) {
40
42
  })
41
43
  }, {
42
44
  projectPath,
43
- fileSystem: dependencies.fileSystem || fs,
45
+ fileSystem,
44
46
  sessionStore
45
47
  });
46
48
  }
@@ -48,7 +50,7 @@ async function runSpecPipeline(options = {}, dependencies = {}) {
48
50
  const specId = specTargets[0];
49
51
 
50
52
  const specPath = path.join(projectPath, '.sce', 'specs', specId);
51
- if (!await fs.pathExists(specPath)) {
53
+ if (!await fileSystem.pathExists(specPath)) {
52
54
  throw new Error(`Spec not found: ${specId}`);
53
55
  }
54
56
 
@@ -57,7 +59,7 @@ async function runSpecPipeline(options = {}, dependencies = {}) {
57
59
  allowNoScene: false
58
60
  }, {
59
61
  projectPath,
60
- fileSystem: dependencies.fileSystem || fs,
62
+ fileSystem,
61
63
  sessionStore
62
64
  });
63
65
 
@@ -130,6 +132,11 @@ async function runSpecPipeline(options = {}, dependencies = {}) {
130
132
  status: execution.status,
131
133
  stage_results: execution.stageResults,
132
134
  failure: execution.failure,
135
+ strategy_assessment: await buildStrategyAssessment(specId, {
136
+ projectPath,
137
+ fileSystem,
138
+ strategyAssessor: dependencies.strategyAssessor
139
+ }),
133
140
  next_actions: buildNextActions(execution),
134
141
  state_file: path.relative(projectPath, stateStore.getRunPath(specId, state.run_id)),
135
142
  scene_session: sceneBinding
@@ -156,7 +163,8 @@ async function runSpecPipeline(options = {}, dependencies = {}) {
156
163
  spec: specId,
157
164
  run_id: state.run_id,
158
165
  pipeline_status: execution.status,
159
- failure: execution.failure || null
166
+ failure: execution.failure || null,
167
+ strategy_decision: result.strategy_assessment ? result.strategy_assessment.decision : null
160
168
  }
161
169
  });
162
170
  }
@@ -174,6 +182,7 @@ async function runSpecPipeline(options = {}, dependencies = {}) {
174
182
  console.log(JSON.stringify(result, null, 2));
175
183
  } else {
176
184
  printResult(result);
185
+ emitStrategyAdvisory(result.strategy_assessment, options);
177
186
  }
178
187
 
179
188
  return result;
@@ -0,0 +1,73 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs-extra');
3
+ const { assessComplexityStrategy } = require('../spec/complexity-strategy');
4
+
5
+ function normalizeText(value) {
6
+ if (typeof value !== 'string') {
7
+ return '';
8
+ }
9
+ return value.trim();
10
+ }
11
+
12
+ function printPayload(payload, options = {}) {
13
+ if (options.json) {
14
+ console.log(JSON.stringify(payload, null, 2));
15
+ return;
16
+ }
17
+
18
+ console.log(chalk.blue('Spec Strategy Assess'));
19
+ console.log(` Source: ${payload.source.type}${payload.source.id ? ` (${payload.source.id})` : ''}`);
20
+ console.log(` Decision: ${payload.decision}`);
21
+ console.log(` Reason: ${payload.decision_reason}`);
22
+ console.log(` Recommended Program Specs: ${payload.summary.recommended_program_specs}`);
23
+ if (Array.isArray(payload.signals) && payload.signals.length > 0) {
24
+ console.log(chalk.gray(' Signals:'));
25
+ payload.signals.forEach((signal) => {
26
+ console.log(chalk.gray(` - ${signal}`));
27
+ });
28
+ }
29
+ }
30
+
31
+ async function runSpecStrategyAssessCommand(options = {}, dependencies = {}) {
32
+ const projectPath = dependencies.projectPath || process.cwd();
33
+ const fileSystem = dependencies.fileSystem || fs;
34
+ const payload = await assessComplexityStrategy({
35
+ goal: normalizeText(options.goal),
36
+ spec: normalizeText(options.spec)
37
+ }, {
38
+ projectPath,
39
+ fileSystem
40
+ });
41
+ printPayload(payload, options);
42
+ return payload;
43
+ }
44
+
45
+ function registerSpecStrategyCommand(program) {
46
+ const strategy = program
47
+ .command('spec-strategy')
48
+ .description('Assess whether a problem should stay single-spec or escalate to a coordinated program');
49
+
50
+ strategy
51
+ .command('assess')
52
+ .description('Assess complexity and recommend single-spec vs multi-spec-program vs research-program')
53
+ .option('--goal <text>', 'Broad goal or problem statement to assess')
54
+ .option('--spec <spec-id>', 'Existing spec to assess from current artifacts')
55
+ .option('--json', 'Output machine-readable JSON')
56
+ .action(async (options) => {
57
+ try {
58
+ await runSpecStrategyAssessCommand(options);
59
+ } catch (error) {
60
+ if (options.json) {
61
+ console.log(JSON.stringify({ success: false, error: error.message }, null, 2));
62
+ } else {
63
+ console.error(chalk.red('❌ spec-strategy assess failed:'), error.message);
64
+ }
65
+ process.exit(1);
66
+ }
67
+ });
68
+ }
69
+
70
+ module.exports = {
71
+ runSpecStrategyAssessCommand,
72
+ registerSpecStrategyCommand
73
+ };
@@ -82,6 +82,31 @@ function toRelativePosix(projectPath, absolutePath) {
82
82
  return path.relative(projectPath, absolutePath).replace(/\\/g, '/');
83
83
  }
84
84
 
85
+ function sortKeysDeep(value) {
86
+ if (Array.isArray(value)) {
87
+ return value.map((item) => sortKeysDeep(item));
88
+ }
89
+ if (!value || typeof value !== 'object') {
90
+ return value;
91
+ }
92
+ const sorted = {};
93
+ Object.keys(value).sort().forEach((key) => {
94
+ sorted[key] = sortKeysDeep(value[key]);
95
+ });
96
+ return sorted;
97
+ }
98
+
99
+ function toComparableProjection(payload) {
100
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
101
+ return null;
102
+ }
103
+ const clone = {
104
+ ...payload
105
+ };
106
+ delete clone.generated_at;
107
+ return sortKeysDeep(clone);
108
+ }
109
+
85
110
  async function buildProjectSharedProblemProjection(projectPath = process.cwd(), options = {}, dependencies = {}) {
86
111
  const fileSystem = dependencies.fileSystem || fs;
87
112
  const studioIntakePolicy = dependencies.studioIntakePolicy || await loadStudioIntakePolicy(projectPath, fileSystem);
@@ -212,6 +237,24 @@ async function syncProjectSharedProblemProjection(projectPath = process.cwd(), o
212
237
  ...dependencies,
213
238
  problemClosurePolicyBundle: closurePolicy
214
239
  });
240
+ const existing = await readJsonSafe(absolutePath, fileSystem);
241
+ const existingComparable = toComparableProjection(existing);
242
+ const nextComparable = toComparableProjection(payload);
243
+
244
+ if (
245
+ existingComparable
246
+ && nextComparable
247
+ && JSON.stringify(existingComparable) === JSON.stringify(nextComparable)
248
+ ) {
249
+ return {
250
+ mode: 'project-problem-projection-sync',
251
+ enabled: true,
252
+ file: absolutePath,
253
+ scope: projectionConfig.scope,
254
+ total_entries: Number(payload.summary.total_entries || payload.entries.length || 0),
255
+ refreshed: false
256
+ };
257
+ }
215
258
 
216
259
  await fileSystem.ensureDir(path.dirname(absolutePath));
217
260
  await fileSystem.writeJson(absolutePath, payload, { spaces: 2 });