@bhargavvc/sdd-cc 1.30.0 → 1.35.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/README.ja-JP.md +144 -110
- package/README.ko-KR.md +143 -107
- package/README.md +183 -112
- package/README.pt-BR.md +90 -52
- package/README.zh-CN.md +141 -101
- package/agents/sdd-advisor-researcher.md +23 -0
- package/agents/sdd-ai-researcher.md +133 -0
- package/agents/sdd-code-fixer.md +516 -0
- package/agents/sdd-code-reviewer.md +355 -0
- package/agents/sdd-codebase-mapper.md +3 -3
- package/agents/sdd-debugger.md +17 -5
- package/agents/sdd-doc-verifier.md +201 -0
- package/agents/sdd-doc-writer.md +602 -0
- package/agents/sdd-domain-researcher.md +153 -0
- package/agents/sdd-eval-auditor.md +164 -0
- package/agents/sdd-eval-planner.md +154 -0
- package/agents/sdd-executor.md +87 -4
- package/agents/sdd-framework-selector.md +160 -0
- package/agents/sdd-intel-updater.md +314 -0
- package/agents/sdd-nyquist-auditor.md +1 -1
- package/agents/sdd-phase-researcher.md +71 -4
- package/agents/sdd-plan-checker.md +100 -6
- package/agents/sdd-planner.md +145 -206
- package/agents/sdd-project-researcher.md +25 -2
- package/agents/sdd-research-synthesizer.md +3 -3
- package/agents/sdd-roadmapper.md +6 -6
- package/agents/sdd-security-auditor.md +128 -0
- package/agents/sdd-ui-auditor.md +43 -3
- package/agents/sdd-ui-checker.md +5 -5
- package/agents/sdd-ui-researcher.md +27 -4
- package/agents/sdd-user-profiler.md +2 -2
- package/agents/sdd-verifier.md +142 -22
- package/bin/install.js +2151 -551
- package/commands/sdd/add-backlog.md +5 -5
- package/commands/sdd/add-tests.md +2 -2
- package/commands/sdd/ai-integration-phase.md +36 -0
- package/commands/sdd/analyze-dependencies.md +34 -0
- package/commands/sdd/audit-fix.md +33 -0
- package/commands/sdd/autonomous.md +7 -2
- package/commands/sdd/cleanup.md +5 -0
- package/commands/sdd/code-review-fix.md +52 -0
- package/commands/sdd/code-review.md +55 -0
- package/commands/sdd/complete-milestone.md +6 -6
- package/commands/sdd/debug.md +22 -9
- package/commands/sdd/discuss-phase.md +7 -2
- package/commands/sdd/do.md +1 -1
- package/commands/sdd/docs-update.md +48 -0
- package/commands/sdd/eval-review.md +32 -0
- package/commands/sdd/execute-phase.md +4 -0
- package/commands/sdd/explore.md +27 -0
- package/commands/sdd/fast.md +2 -2
- package/commands/sdd/from-sdd2.md +45 -0
- package/commands/sdd/help.md +2 -0
- package/commands/sdd/import.md +36 -0
- package/commands/sdd/intel.md +179 -0
- package/commands/sdd/join-discord.md +2 -1
- package/commands/sdd/manager.md +1 -0
- package/commands/sdd/map-codebase.md +3 -3
- package/commands/sdd/new-milestone.md +1 -1
- package/commands/sdd/new-project.md +5 -1
- package/commands/sdd/new-workspace.md +1 -1
- package/commands/sdd/next.md +2 -0
- package/commands/sdd/plan-milestone-gaps.md +2 -2
- package/commands/sdd/plan-phase.md +6 -1
- package/commands/sdd/plant-seed.md +1 -1
- package/commands/sdd/profile-user.md +1 -1
- package/commands/sdd/quick.md +5 -3
- package/commands/sdd/reapply-patches.md +230 -42
- package/commands/sdd/research-phase.md +3 -3
- package/commands/sdd/review-backlog.md +1 -0
- package/commands/sdd/review.md +6 -3
- package/commands/sdd/scan.md +26 -0
- package/commands/sdd/secure-phase.md +35 -0
- package/commands/sdd/ship.md +1 -1
- package/commands/sdd/thread.md +5 -5
- package/commands/sdd/undo.md +34 -0
- package/commands/sdd/verify-work.md +1 -1
- package/commands/sdd/workstreams.md +17 -11
- package/hooks/dist/sdd-check-update.js +33 -8
- package/hooks/dist/sdd-context-monitor.js +17 -8
- package/hooks/dist/sdd-phase-boundary.sh +27 -0
- package/hooks/dist/sdd-prompt-guard.js +1 -0
- package/hooks/dist/sdd-read-guard.js +82 -0
- package/hooks/dist/sdd-session-state.sh +33 -0
- package/hooks/dist/sdd-statusline.js +137 -15
- package/hooks/dist/sdd-validate-commit.sh +47 -0
- package/hooks/dist/sdd-workflow-guard.js +4 -4
- package/hooks/sdd-check-update.js +139 -0
- package/hooks/sdd-context-monitor.js +165 -0
- package/hooks/sdd-phase-boundary.sh +27 -0
- package/hooks/sdd-prompt-guard.js +97 -0
- package/hooks/sdd-read-guard.js +82 -0
- package/hooks/sdd-session-state.sh +33 -0
- package/hooks/sdd-statusline.js +241 -0
- package/hooks/sdd-validate-commit.sh +47 -0
- package/hooks/sdd-workflow-guard.js +94 -0
- package/package.json +3 -3
- package/scripts/build-hooks.js +18 -7
- package/scripts/prompt-injection-scan.sh +1 -0
- package/scripts/rebrand-gsd-to-sdd.sh +221 -220
- package/scripts/run-tests.cjs +5 -1
- package/scripts/sync-upstream.sh +1 -1
- package/sdd/bin/lib/commands.cjs +79 -17
- package/sdd/bin/lib/config.cjs +90 -48
- package/sdd/bin/lib/core.cjs +452 -87
- package/sdd/bin/lib/docs.cjs +267 -0
- package/sdd/bin/lib/frontmatter.cjs +381 -336
- package/sdd/bin/lib/init.cjs +110 -16
- package/sdd/bin/lib/intel.cjs +660 -0
- package/sdd/bin/lib/learnings.cjs +378 -0
- package/sdd/bin/lib/milestone.cjs +42 -11
- package/sdd/bin/lib/model-profiles.cjs +17 -15
- package/sdd/bin/lib/phase.cjs +367 -288
- package/sdd/bin/lib/profile-output.cjs +106 -10
- package/sdd/bin/lib/roadmap.cjs +146 -115
- package/sdd/bin/lib/schema-detect.cjs +238 -0
- package/sdd/bin/lib/sdd2-import.cjs +511 -0
- package/sdd/bin/lib/security.cjs +124 -3
- package/sdd/bin/lib/state.cjs +648 -264
- package/sdd/bin/lib/template.cjs +8 -4
- package/sdd/bin/lib/verify.cjs +209 -28
- package/sdd/bin/lib/workstream.cjs +7 -3
- package/sdd/bin/sdd-tools.cjs +184 -12
- package/sdd/contexts/dev.md +21 -0
- package/sdd/contexts/research.md +22 -0
- package/sdd/contexts/review.md +22 -0
- package/sdd/references/agent-contracts.md +79 -0
- package/sdd/references/ai-evals.md +156 -0
- package/sdd/references/ai-frameworks.md +186 -0
- package/sdd/references/artifact-types.md +113 -0
- package/sdd/references/common-bug-patterns.md +114 -0
- package/sdd/references/context-budget.md +49 -0
- package/sdd/references/continuation-format.md +25 -25
- package/sdd/references/domain-probes.md +125 -0
- package/sdd/references/few-shot-examples/plan-checker.md +73 -0
- package/sdd/references/few-shot-examples/verifier.md +109 -0
- package/sdd/references/gate-prompts.md +100 -0
- package/sdd/references/gates.md +70 -0
- package/sdd/references/git-integration.md +1 -1
- package/sdd/references/ios-scaffold.md +123 -0
- package/sdd/references/model-profile-resolution.md +2 -0
- package/sdd/references/model-profiles.md +24 -18
- package/sdd/references/planner-gap-closure.md +62 -0
- package/sdd/references/planner-reviews.md +39 -0
- package/sdd/references/planner-revision.md +87 -0
- package/sdd/references/planning-config.md +252 -0
- package/sdd/references/revision-loop.md +97 -0
- package/sdd/references/thinking-models-debug.md +44 -0
- package/sdd/references/thinking-models-execution.md +50 -0
- package/sdd/references/thinking-models-planning.md +62 -0
- package/sdd/references/thinking-models-research.md +50 -0
- package/sdd/references/thinking-models-verification.md +55 -0
- package/sdd/references/thinking-partner.md +96 -0
- package/sdd/references/ui-brand.md +4 -4
- package/sdd/references/universal-anti-patterns.md +63 -0
- package/sdd/references/verification-overrides.md +227 -0
- package/sdd/references/workstream-flag.md +56 -3
- package/sdd/templates/AI-SPEC.md +246 -0
- package/sdd/templates/DEBUG.md +1 -1
- package/sdd/templates/SECURITY.md +61 -0
- package/sdd/templates/UAT.md +4 -4
- package/sdd/templates/VALIDATION.md +4 -4
- package/sdd/templates/claude-md.md +32 -9
- package/sdd/templates/config.json +4 -0
- package/sdd/templates/debug-subagent-prompt.md +1 -1
- package/sdd/templates/dev-preferences.md +1 -1
- package/sdd/templates/discovery.md +2 -2
- package/sdd/templates/phase-prompt.md +1 -1
- package/sdd/templates/planner-subagent-prompt.md +3 -3
- package/sdd/templates/project.md +1 -1
- package/sdd/templates/research.md +1 -1
- package/sdd/templates/state.md +2 -2
- package/sdd/workflows/add-phase.md +8 -8
- package/sdd/workflows/add-tests.md +12 -9
- package/sdd/workflows/add-todo.md +5 -3
- package/sdd/workflows/ai-integration-phase.md +284 -0
- package/sdd/workflows/analyze-dependencies.md +96 -0
- package/sdd/workflows/audit-fix.md +157 -0
- package/sdd/workflows/audit-milestone.md +11 -11
- package/sdd/workflows/audit-uat.md +2 -2
- package/sdd/workflows/autonomous.md +195 -27
- package/sdd/workflows/check-todos.md +12 -10
- package/sdd/workflows/cleanup.md +2 -0
- package/sdd/workflows/code-review-fix.md +497 -0
- package/sdd/workflows/code-review.md +515 -0
- package/sdd/workflows/complete-milestone.md +56 -22
- package/sdd/workflows/diagnose-issues.md +10 -3
- package/sdd/workflows/discovery-phase.md +5 -3
- package/sdd/workflows/discuss-phase-assumptions.md +24 -6
- package/sdd/workflows/discuss-phase-power.md +291 -0
- package/sdd/workflows/discuss-phase.md +173 -21
- package/sdd/workflows/do.md +23 -21
- package/sdd/workflows/docs-update.md +1155 -0
- package/sdd/workflows/eval-review.md +155 -0
- package/sdd/workflows/execute-phase.md +594 -38
- package/sdd/workflows/execute-plan.md +67 -96
- package/sdd/workflows/explore.md +139 -0
- package/sdd/workflows/fast.md +5 -5
- package/sdd/workflows/forensics.md +2 -2
- package/sdd/workflows/health.md +4 -4
- package/sdd/workflows/help.md +122 -119
- package/sdd/workflows/import.md +276 -0
- package/sdd/workflows/inbox.md +387 -0
- package/sdd/workflows/insert-phase.md +7 -7
- package/sdd/workflows/list-phase-assumptions.md +4 -4
- package/sdd/workflows/list-workspaces.md +2 -2
- package/sdd/workflows/manager.md +35 -32
- package/sdd/workflows/map-codebase.md +7 -5
- package/sdd/workflows/milestone-summary.md +2 -2
- package/sdd/workflows/new-milestone.md +17 -9
- package/sdd/workflows/new-project.md +50 -25
- package/sdd/workflows/new-workspace.md +7 -5
- package/sdd/workflows/next.md +67 -11
- package/sdd/workflows/note.md +9 -7
- package/sdd/workflows/pause-work.md +75 -12
- package/sdd/workflows/plan-milestone-gaps.md +8 -8
- package/sdd/workflows/plan-phase.md +294 -42
- package/sdd/workflows/plant-seed.md +6 -3
- package/sdd/workflows/pr-branch.md +42 -14
- package/sdd/workflows/profile-user.md +9 -7
- package/sdd/workflows/progress.md +45 -45
- package/sdd/workflows/quick.md +195 -47
- package/sdd/workflows/remove-phase.md +6 -6
- package/sdd/workflows/remove-workspace.md +3 -1
- package/sdd/workflows/research-phase.md +2 -2
- package/sdd/workflows/resume-project.md +12 -12
- package/sdd/workflows/review.md +109 -9
- package/sdd/workflows/scan.md +102 -0
- package/sdd/workflows/secure-phase.md +166 -0
- package/sdd/workflows/session-report.md +2 -2
- package/sdd/workflows/settings.md +38 -12
- package/sdd/workflows/ship.md +21 -9
- package/sdd/workflows/stats.md +1 -1
- package/sdd/workflows/transition.md +23 -23
- package/sdd/workflows/ui-phase.md +15 -7
- package/sdd/workflows/ui-review.md +29 -4
- package/sdd/workflows/undo.md +314 -0
- package/sdd/workflows/update.md +171 -20
- package/sdd/workflows/validate-phase.md +6 -4
- package/sdd/workflows/verify-phase.md +210 -6
- package/sdd/workflows/verify-work.md +83 -9
- package/sdd/commands/sdd/workstreams.md +0 -63
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* sdd2-import — Reverse migration from SDD-2 (.sdd/) to SDD v1 (.planning/)
|
|
5
|
+
*
|
|
6
|
+
* Reads a SDD-2 project directory structure and produces a complete
|
|
7
|
+
* .planning/ artifact tree in SDD v1 format.
|
|
8
|
+
*
|
|
9
|
+
* SDD-2 hierarchy: Milestone → Slice → Task
|
|
10
|
+
* SDD v1 hierarchy: Milestone (in ROADMAP.md) → Phase → Plan
|
|
11
|
+
*
|
|
12
|
+
* Mapping rules:
|
|
13
|
+
* - Slices are numbered sequentially across all milestones (01, 02, …)
|
|
14
|
+
* - Tasks within a slice become plans (01-01, 01-02, …)
|
|
15
|
+
* - Completed slices ([x] in ROADMAP) → [x] phases in ROADMAP.md
|
|
16
|
+
* - Tasks with a SUMMARY file → SUMMARY.md written
|
|
17
|
+
* - Slice RESEARCH.md → phase XX-RESEARCH.md
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
|
|
23
|
+
// ─── Utilities ──────────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
function readOptional(filePath) {
|
|
26
|
+
try { return fs.readFileSync(filePath, 'utf8'); } catch { return null; }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function zeroPad(n, width = 2) {
|
|
30
|
+
return String(n).padStart(width, '0');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function slugify(title) {
|
|
34
|
+
return title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── SDD-2 Parser ───────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Find the .sdd/ directory starting from a project root.
|
|
41
|
+
* Returns the absolute path or null if not found.
|
|
42
|
+
*/
|
|
43
|
+
function findSdd2Root(startPath) {
|
|
44
|
+
if (path.basename(startPath) === '.sdd' && fs.existsSync(startPath)) {
|
|
45
|
+
return startPath;
|
|
46
|
+
}
|
|
47
|
+
const candidate = path.join(startPath, '.sdd');
|
|
48
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) {
|
|
49
|
+
return candidate;
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse the ## Slices section from a SDD-2 milestone ROADMAP.md.
|
|
56
|
+
* Each slice entry looks like:
|
|
57
|
+
* - [x] **S01: Title** `risk:medium` `depends:[S00]`
|
|
58
|
+
*/
|
|
59
|
+
function parseSlicesFromRoadmap(content) {
|
|
60
|
+
const slices = [];
|
|
61
|
+
const sectionMatch = content.match(/## Slices\n([\s\S]*?)(?:\n## |\n# |$)/);
|
|
62
|
+
if (!sectionMatch) return slices;
|
|
63
|
+
|
|
64
|
+
for (const line of sectionMatch[1].split('\n')) {
|
|
65
|
+
const m = line.match(/^- \[([x ])\]\s+\*\*(\w+):\s*([^*]+)\*\*/);
|
|
66
|
+
if (!m) continue;
|
|
67
|
+
slices.push({ done: m[1] === 'x', id: m[2].trim(), title: m[3].trim() });
|
|
68
|
+
}
|
|
69
|
+
return slices;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parse the milestone title from the first heading in a SDD-2 ROADMAP.md.
|
|
74
|
+
* Format: # M001: Title
|
|
75
|
+
*/
|
|
76
|
+
function parseMilestoneTitle(content) {
|
|
77
|
+
const m = content.match(/^# \w+:\s*(.+)/m);
|
|
78
|
+
return m ? m[1].trim() : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Parse a task title from a SDD-2 T##-PLAN.md.
|
|
83
|
+
* Format: # T01: Title
|
|
84
|
+
*/
|
|
85
|
+
function parseTaskTitle(content, fallback) {
|
|
86
|
+
const m = content.match(/^# \w+:\s*(.+)/m);
|
|
87
|
+
return m ? m[1].trim() : fallback;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Parse the ## Description body from a SDD-2 task plan.
|
|
92
|
+
*/
|
|
93
|
+
function parseTaskDescription(content) {
|
|
94
|
+
const m = content.match(/## Description\n+([\s\S]+?)(?:\n## |\n# |$)/);
|
|
95
|
+
return m ? m[1].trim() : '';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Parse ## Must-Haves items from a SDD-2 task plan.
|
|
100
|
+
*/
|
|
101
|
+
function parseTaskMustHaves(content) {
|
|
102
|
+
const m = content.match(/## Must-Haves\n+([\s\S]+?)(?:\n## |\n# |$)/);
|
|
103
|
+
if (!m) return [];
|
|
104
|
+
return m[1].split('\n')
|
|
105
|
+
.map(l => l.match(/^- \[[ x]\]\s*(.+)/))
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.map(match => match[1].trim());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Read all task plan files from a SDD-2 tasks/ directory.
|
|
112
|
+
*/
|
|
113
|
+
function readTasksDir(tasksDir) {
|
|
114
|
+
if (!fs.existsSync(tasksDir)) return [];
|
|
115
|
+
|
|
116
|
+
return fs.readdirSync(tasksDir)
|
|
117
|
+
.filter(f => f.endsWith('-PLAN.md'))
|
|
118
|
+
.sort()
|
|
119
|
+
.map(tf => {
|
|
120
|
+
const tid = tf.replace('-PLAN.md', '');
|
|
121
|
+
const plan = readOptional(path.join(tasksDir, tf));
|
|
122
|
+
const summary = readOptional(path.join(tasksDir, `${tid}-SUMMARY.md`));
|
|
123
|
+
return {
|
|
124
|
+
id: tid,
|
|
125
|
+
title: plan ? parseTaskTitle(plan, tid) : tid,
|
|
126
|
+
description: plan ? parseTaskDescription(plan) : '',
|
|
127
|
+
mustHaves: plan ? parseTaskMustHaves(plan) : [],
|
|
128
|
+
plan,
|
|
129
|
+
summary,
|
|
130
|
+
done: !!summary,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Parse a complete SDD-2 .sdd/ directory into a structured representation.
|
|
137
|
+
*/
|
|
138
|
+
function parseSdd2(sddDir) {
|
|
139
|
+
const data = {
|
|
140
|
+
projectContent: readOptional(path.join(sddDir, 'PROJECT.md')),
|
|
141
|
+
requirements: readOptional(path.join(sddDir, 'REQUIREMENTS.md')),
|
|
142
|
+
milestones: [],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const milestonesBase = path.join(sddDir, 'milestones');
|
|
146
|
+
if (!fs.existsSync(milestonesBase)) return data;
|
|
147
|
+
|
|
148
|
+
const milestoneIds = fs.readdirSync(milestonesBase)
|
|
149
|
+
.filter(d => fs.statSync(path.join(milestonesBase, d)).isDirectory())
|
|
150
|
+
.sort();
|
|
151
|
+
|
|
152
|
+
for (const mid of milestoneIds) {
|
|
153
|
+
const mDir = path.join(milestonesBase, mid);
|
|
154
|
+
const roadmapContent = readOptional(path.join(mDir, `${mid}-ROADMAP.md`));
|
|
155
|
+
const slicesDir = path.join(mDir, 'slices');
|
|
156
|
+
|
|
157
|
+
const sliceInfos = roadmapContent ? parseSlicesFromRoadmap(roadmapContent) : [];
|
|
158
|
+
|
|
159
|
+
const slices = sliceInfos.map(info => {
|
|
160
|
+
const sDir = path.join(slicesDir, info.id);
|
|
161
|
+
const hasSDir = fs.existsSync(sDir);
|
|
162
|
+
return {
|
|
163
|
+
id: info.id,
|
|
164
|
+
title: info.title,
|
|
165
|
+
done: info.done,
|
|
166
|
+
plan: hasSDir ? readOptional(path.join(sDir, `${info.id}-PLAN.md`)) : null,
|
|
167
|
+
summary: hasSDir ? readOptional(path.join(sDir, `${info.id}-SUMMARY.md`)) : null,
|
|
168
|
+
research: hasSDir ? readOptional(path.join(sDir, `${info.id}-RESEARCH.md`)) : null,
|
|
169
|
+
context: hasSDir ? readOptional(path.join(sDir, `${info.id}-CONTEXT.md`)) : null,
|
|
170
|
+
tasks: hasSDir ? readTasksDir(path.join(sDir, 'tasks')) : [],
|
|
171
|
+
};
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
data.milestones.push({
|
|
175
|
+
id: mid,
|
|
176
|
+
title: roadmapContent ? (parseMilestoneTitle(roadmapContent) ?? mid) : mid,
|
|
177
|
+
research: readOptional(path.join(mDir, `${mid}-RESEARCH.md`)),
|
|
178
|
+
slices,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return data;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── Artifact Builders ──────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Build a SDD v1 PLAN.md from a SDD-2 task.
|
|
189
|
+
*/
|
|
190
|
+
function buildPlanMd(task, phasePrefix, planPrefix, phaseSlug, milestoneTitle) {
|
|
191
|
+
const lines = [
|
|
192
|
+
'---',
|
|
193
|
+
`phase: "${phasePrefix}"`,
|
|
194
|
+
`plan: "${planPrefix}"`,
|
|
195
|
+
'type: "implementation"',
|
|
196
|
+
'---',
|
|
197
|
+
'',
|
|
198
|
+
'<objective>',
|
|
199
|
+
task.title,
|
|
200
|
+
'</objective>',
|
|
201
|
+
'',
|
|
202
|
+
'<context>',
|
|
203
|
+
`Phase: ${phasePrefix} (${phaseSlug}) — Milestone: ${milestoneTitle}`,
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
if (task.description) {
|
|
207
|
+
lines.push('', task.description);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
lines.push('</context>');
|
|
211
|
+
|
|
212
|
+
if (task.mustHaves.length > 0) {
|
|
213
|
+
lines.push('', '<must_haves>');
|
|
214
|
+
for (const mh of task.mustHaves) {
|
|
215
|
+
lines.push(`- ${mh}`);
|
|
216
|
+
}
|
|
217
|
+
lines.push('</must_haves>');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return lines.join('\n') + '\n';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Build a SDD v1 SUMMARY.md from a SDD-2 task summary.
|
|
225
|
+
* Strips the SDD-2 frontmatter and preserves the body.
|
|
226
|
+
*/
|
|
227
|
+
function buildSummaryMd(task, phasePrefix, planPrefix) {
|
|
228
|
+
const raw = task.summary || '';
|
|
229
|
+
// Strip SDD-2 frontmatter block (--- ... ---) if present
|
|
230
|
+
const bodyMatch = raw.match(/^---[\s\S]*?---\n+([\s\S]*)$/);
|
|
231
|
+
const body = bodyMatch ? bodyMatch[1].trim() : raw.trim();
|
|
232
|
+
|
|
233
|
+
return [
|
|
234
|
+
'---',
|
|
235
|
+
`phase: "${phasePrefix}"`,
|
|
236
|
+
`plan: "${planPrefix}"`,
|
|
237
|
+
'---',
|
|
238
|
+
'',
|
|
239
|
+
body || 'Task completed (migrated from SDD-2).',
|
|
240
|
+
'',
|
|
241
|
+
].join('\n');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Build a SDD v1 XX-CONTEXT.md from a SDD-2 slice.
|
|
246
|
+
*/
|
|
247
|
+
function buildContextMd(slice, phasePrefix) {
|
|
248
|
+
const lines = [
|
|
249
|
+
`# Phase ${phasePrefix} Context`,
|
|
250
|
+
'',
|
|
251
|
+
`Migrated from SDD-2 slice ${slice.id}: ${slice.title}`,
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
const extra = slice.context || '';
|
|
255
|
+
if (extra.trim()) {
|
|
256
|
+
lines.push('', extra.trim());
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return lines.join('\n') + '\n';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Build the SDD v1 ROADMAP.md with milestone-sectioned format.
|
|
264
|
+
*/
|
|
265
|
+
function buildRoadmapMd(milestones, phaseMap) {
|
|
266
|
+
const lines = ['# Roadmap', ''];
|
|
267
|
+
|
|
268
|
+
for (const milestone of milestones) {
|
|
269
|
+
lines.push(`## ${milestone.id}: ${milestone.title}`, '');
|
|
270
|
+
const mPhases = phaseMap.filter(p => p.milestoneId === milestone.id);
|
|
271
|
+
for (const { slice, phaseNum } of mPhases) {
|
|
272
|
+
const prefix = zeroPad(phaseNum);
|
|
273
|
+
const slug = slugify(slice.title);
|
|
274
|
+
const check = slice.done ? 'x' : ' ';
|
|
275
|
+
lines.push(`- [${check}] **Phase ${prefix}: ${slug}** — ${slice.title}`);
|
|
276
|
+
}
|
|
277
|
+
lines.push('');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return lines.join('\n');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Build the SDD v1 STATE.md reflecting the current position in the project.
|
|
285
|
+
*/
|
|
286
|
+
function buildStateMd(phaseMap) {
|
|
287
|
+
const currentEntry = phaseMap.find(p => !p.slice.done);
|
|
288
|
+
const totalPhases = phaseMap.length;
|
|
289
|
+
const donePhases = phaseMap.filter(p => p.slice.done).length;
|
|
290
|
+
const pct = totalPhases > 0 ? Math.round((donePhases / totalPhases) * 100) : 0;
|
|
291
|
+
|
|
292
|
+
const currentPhaseNum = currentEntry ? zeroPad(currentEntry.phaseNum) : zeroPad(totalPhases);
|
|
293
|
+
const currentSlug = currentEntry ? slugify(currentEntry.slice.title) : 'complete';
|
|
294
|
+
const status = currentEntry ? 'Ready to plan' : 'All phases complete';
|
|
295
|
+
|
|
296
|
+
const filled = Math.round(pct / 10);
|
|
297
|
+
const bar = `[${'█'.repeat(filled)}${'░'.repeat(10 - filled)}]`;
|
|
298
|
+
const today = new Date().toISOString().split('T')[0];
|
|
299
|
+
|
|
300
|
+
return [
|
|
301
|
+
'# Project State',
|
|
302
|
+
'',
|
|
303
|
+
'## Project Reference',
|
|
304
|
+
'',
|
|
305
|
+
'See: .planning/PROJECT.md',
|
|
306
|
+
'',
|
|
307
|
+
`**Current focus:** Phase ${currentPhaseNum} (${currentSlug})`,
|
|
308
|
+
'',
|
|
309
|
+
'## Current Position',
|
|
310
|
+
'',
|
|
311
|
+
`Phase: ${currentPhaseNum} of ${zeroPad(totalPhases)} (${currentSlug})`,
|
|
312
|
+
`Status: ${status}`,
|
|
313
|
+
`Last activity: ${today} — Migrated from SDD-2`,
|
|
314
|
+
'',
|
|
315
|
+
`Progress: ${bar} ${pct}%`,
|
|
316
|
+
'',
|
|
317
|
+
'## Accumulated Context',
|
|
318
|
+
'',
|
|
319
|
+
'### Decisions',
|
|
320
|
+
'',
|
|
321
|
+
'Migrated from SDD-2. Review PROJECT.md for key decisions.',
|
|
322
|
+
'',
|
|
323
|
+
'### Blockers/Concerns',
|
|
324
|
+
'',
|
|
325
|
+
'None.',
|
|
326
|
+
'',
|
|
327
|
+
'## Session Continuity',
|
|
328
|
+
'',
|
|
329
|
+
`Last session: ${today}`,
|
|
330
|
+
'Stopped at: Migration from SDD-2 completed',
|
|
331
|
+
'Resume file: None',
|
|
332
|
+
'',
|
|
333
|
+
].join('\n');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ─── Transformer ─────────────────────────────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Convert parsed SDD-2 data into a map of relative path → file content.
|
|
340
|
+
* All paths are relative to the .planning/ root.
|
|
341
|
+
*/
|
|
342
|
+
function buildPlanningArtifacts(sdd2Data) {
|
|
343
|
+
const artifacts = new Map();
|
|
344
|
+
|
|
345
|
+
// Passthrough files
|
|
346
|
+
artifacts.set('PROJECT.md', sdd2Data.projectContent || '# Project\n\n(Migrated from SDD-2)\n');
|
|
347
|
+
if (sdd2Data.requirements) {
|
|
348
|
+
artifacts.set('REQUIREMENTS.md', sdd2Data.requirements);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Minimal valid v1 config
|
|
352
|
+
artifacts.set('config.json', JSON.stringify({ version: 1 }, null, 2) + '\n');
|
|
353
|
+
|
|
354
|
+
// Build sequential phase map: flatten Milestones → Slices into numbered phases
|
|
355
|
+
const phaseMap = [];
|
|
356
|
+
let phaseNum = 1;
|
|
357
|
+
for (const milestone of sdd2Data.milestones) {
|
|
358
|
+
for (const slice of milestone.slices) {
|
|
359
|
+
phaseMap.push({ milestoneId: milestone.id, milestoneTitle: milestone.title, slice, phaseNum });
|
|
360
|
+
phaseNum++;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
artifacts.set('ROADMAP.md', buildRoadmapMd(sdd2Data.milestones, phaseMap));
|
|
365
|
+
artifacts.set('STATE.md', buildStateMd(phaseMap));
|
|
366
|
+
|
|
367
|
+
for (const { slice, phaseNum, milestoneTitle } of phaseMap) {
|
|
368
|
+
const prefix = zeroPad(phaseNum);
|
|
369
|
+
const slug = slugify(slice.title);
|
|
370
|
+
const dir = `phases/${prefix}-${slug}`;
|
|
371
|
+
|
|
372
|
+
artifacts.set(`${dir}/${prefix}-CONTEXT.md`, buildContextMd(slice, prefix));
|
|
373
|
+
|
|
374
|
+
if (slice.research) {
|
|
375
|
+
artifacts.set(`${dir}/${prefix}-RESEARCH.md`, slice.research);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
for (let i = 0; i < slice.tasks.length; i++) {
|
|
379
|
+
const task = slice.tasks[i];
|
|
380
|
+
const planPrefix = zeroPad(i + 1);
|
|
381
|
+
|
|
382
|
+
artifacts.set(
|
|
383
|
+
`${dir}/${prefix}-${planPrefix}-PLAN.md`,
|
|
384
|
+
buildPlanMd(task, prefix, planPrefix, slug, milestoneTitle)
|
|
385
|
+
);
|
|
386
|
+
|
|
387
|
+
if (task.done && task.summary) {
|
|
388
|
+
artifacts.set(
|
|
389
|
+
`${dir}/${prefix}-${planPrefix}-SUMMARY.md`,
|
|
390
|
+
buildSummaryMd(task, prefix, planPrefix)
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return artifacts;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ─── Preview ─────────────────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Format a dry-run preview string for display before writing.
|
|
403
|
+
*/
|
|
404
|
+
function buildPreview(sdd2Data, artifacts) {
|
|
405
|
+
const lines = ['Preview — files that will be created in .planning/:'];
|
|
406
|
+
|
|
407
|
+
for (const rel of artifacts.keys()) {
|
|
408
|
+
lines.push(` ${rel}`);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const totalSlices = sdd2Data.milestones.reduce((s, m) => s + m.slices.length, 0);
|
|
412
|
+
const doneSlices = sdd2Data.milestones.reduce((s, m) => s + m.slices.filter(sl => sl.done).length, 0);
|
|
413
|
+
const allTasks = sdd2Data.milestones.flatMap(m => m.slices.flatMap(sl => sl.tasks));
|
|
414
|
+
const doneTasks = allTasks.filter(t => t.done).length;
|
|
415
|
+
|
|
416
|
+
lines.push('');
|
|
417
|
+
lines.push(`Milestones: ${sdd2Data.milestones.length}`);
|
|
418
|
+
lines.push(`Phases (slices): ${totalSlices} (${doneSlices} completed)`);
|
|
419
|
+
lines.push(`Plans (tasks): ${allTasks.length} (${doneTasks} completed)`);
|
|
420
|
+
lines.push('');
|
|
421
|
+
lines.push('Cannot migrate automatically:');
|
|
422
|
+
lines.push(' - SDD-2 cost/token ledger (no v1 equivalent)');
|
|
423
|
+
lines.push(' - SDD-2 database state (rebuilt from files on first /sdd-health)');
|
|
424
|
+
lines.push(' - VS Code extension state');
|
|
425
|
+
|
|
426
|
+
return lines.join('\n');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ─── Writer ───────────────────────────────────────────────────────────────────
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Write all artifacts to the .planning/ directory.
|
|
433
|
+
*/
|
|
434
|
+
function writePlanningDir(artifacts, planningRoot) {
|
|
435
|
+
for (const [rel, content] of artifacts) {
|
|
436
|
+
const absPath = path.join(planningRoot, rel);
|
|
437
|
+
fs.mkdirSync(path.dirname(absPath), { recursive: true });
|
|
438
|
+
fs.writeFileSync(absPath, content, 'utf8');
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ─── Command Handler ──────────────────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Entry point called from sdd-tools.cjs.
|
|
446
|
+
* Supports: --force, --dry-run, --path <dir>
|
|
447
|
+
*/
|
|
448
|
+
function cmdFromSdd2(args, cwd, raw) {
|
|
449
|
+
const { output, error } = require('./core.cjs');
|
|
450
|
+
|
|
451
|
+
const force = args.includes('--force');
|
|
452
|
+
const dryRun = args.includes('--dry-run');
|
|
453
|
+
|
|
454
|
+
const pathIdx = args.indexOf('--path');
|
|
455
|
+
const projectDir = pathIdx >= 0 && args[pathIdx + 1]
|
|
456
|
+
? path.resolve(cwd, args[pathIdx + 1])
|
|
457
|
+
: cwd;
|
|
458
|
+
|
|
459
|
+
const sddDir = findSdd2Root(projectDir);
|
|
460
|
+
if (!sddDir) {
|
|
461
|
+
return output({ success: false, error: `No .sdd/ directory found in ${projectDir}` }, raw);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const planningRoot = path.join(path.dirname(sddDir), '.planning');
|
|
465
|
+
if (fs.existsSync(planningRoot) && !force) {
|
|
466
|
+
return output({
|
|
467
|
+
success: false,
|
|
468
|
+
error: `.planning/ already exists at ${planningRoot}. Pass --force to overwrite.`,
|
|
469
|
+
}, raw);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const sdd2Data = parseSdd2(sddDir);
|
|
473
|
+
const artifacts = buildPlanningArtifacts(sdd2Data);
|
|
474
|
+
const preview = buildPreview(sdd2Data, artifacts);
|
|
475
|
+
|
|
476
|
+
if (dryRun) {
|
|
477
|
+
return output({ success: true, dryRun: true, preview }, raw);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
writePlanningDir(artifacts, planningRoot);
|
|
481
|
+
|
|
482
|
+
return output({
|
|
483
|
+
success: true,
|
|
484
|
+
planningDir: planningRoot,
|
|
485
|
+
filesWritten: artifacts.size,
|
|
486
|
+
milestones: sdd2Data.milestones.length,
|
|
487
|
+
preview,
|
|
488
|
+
}, raw);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
module.exports = {
|
|
492
|
+
findSdd2Root,
|
|
493
|
+
parseSdd2,
|
|
494
|
+
buildPlanningArtifacts,
|
|
495
|
+
buildPreview,
|
|
496
|
+
writePlanningDir,
|
|
497
|
+
cmdFromSdd2,
|
|
498
|
+
// Exported for unit tests
|
|
499
|
+
parseSlicesFromRoadmap,
|
|
500
|
+
parseMilestoneTitle,
|
|
501
|
+
parseTaskTitle,
|
|
502
|
+
parseTaskDescription,
|
|
503
|
+
parseTaskMustHaves,
|
|
504
|
+
buildPlanMd,
|
|
505
|
+
buildSummaryMd,
|
|
506
|
+
buildContextMd,
|
|
507
|
+
buildRoadmapMd,
|
|
508
|
+
buildStateMd,
|
|
509
|
+
slugify,
|
|
510
|
+
zeroPad,
|
|
511
|
+
};
|
package/sdd/bin/lib/security.cjs
CHANGED
|
@@ -152,6 +152,25 @@ const INJECTION_PATTERNS = [
|
|
|
152
152
|
/(?:run|execute|call|invoke)\s+(?:the\s+)?(?:bash|shell|exec|spawn)\s+(?:tool|command)/i,
|
|
153
153
|
];
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Layer 2: Encoding-obfuscation patterns with custom finding messages.
|
|
157
|
+
* Each entry: { pattern: RegExp, message: string }
|
|
158
|
+
*/
|
|
159
|
+
const OBFUSCATION_PATTERN_ENTRIES = [
|
|
160
|
+
{
|
|
161
|
+
pattern: /\b(\w\s){4,}\w\b/,
|
|
162
|
+
message: 'Character-spacing obfuscation pattern detected (e.g. "i g n o r e")',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
pattern: /<\/?(system|human|assistant|user)\s*>/i,
|
|
166
|
+
message: 'Delimiter injection pattern: <system>/<assistant>/<user> tag detected',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
pattern: /0x[0-9a-fA-F]{16,}/,
|
|
170
|
+
message: 'Long hex sequence detected — possible encoded payload',
|
|
171
|
+
},
|
|
172
|
+
];
|
|
173
|
+
|
|
155
174
|
/**
|
|
156
175
|
* Scan text for potential prompt injection patterns.
|
|
157
176
|
* Returns an array of findings (empty = clean).
|
|
@@ -174,6 +193,13 @@ function scanForInjection(text, opts = {}) {
|
|
|
174
193
|
}
|
|
175
194
|
}
|
|
176
195
|
|
|
196
|
+
// Layer 2: encoding-obfuscation patterns with custom messages
|
|
197
|
+
for (const entry of OBFUSCATION_PATTERN_ENTRIES) {
|
|
198
|
+
if (entry.pattern.test(text)) {
|
|
199
|
+
findings.push(entry.message);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
177
203
|
if (opts.strict) {
|
|
178
204
|
// Check for suspicious Unicode that could hide instructions
|
|
179
205
|
// (zero-width chars, RTL override, homoglyph attacks)
|
|
@@ -181,9 +207,17 @@ function scanForInjection(text, opts = {}) {
|
|
|
181
207
|
findings.push('Contains suspicious zero-width or invisible Unicode characters');
|
|
182
208
|
}
|
|
183
209
|
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
210
|
+
// Layer 1: Unicode tag block U+E0000–U+E007F (2025 supply-chain attack vector)
|
|
211
|
+
// These characters are invisible and can embed hidden instructions
|
|
212
|
+
if (/[\uDB40\uDC00-\uDB40\uDC7F]/u.test(text) || /[\u{E0000}-\u{E007F}]/u.test(text)) {
|
|
213
|
+
findings.push('Contains Unicode tag block characters (U+E0000–E007F) — invisible instruction injection vector');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check for extremely long strings that could be prompt stuffing.
|
|
217
|
+
// Normalize CRLF → LF before measuring so Windows checkouts don't inflate the count.
|
|
218
|
+
const normalizedLength = text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').length;
|
|
219
|
+
if (normalizedLength > 50000) {
|
|
220
|
+
findings.push(`Suspicious text length: ${normalizedLength} chars (potential prompt stuffing)`);
|
|
187
221
|
}
|
|
188
222
|
}
|
|
189
223
|
|
|
@@ -359,6 +393,87 @@ function validateFieldName(field) {
|
|
|
359
393
|
return { valid: false, error: `Invalid field name: "${field}"` };
|
|
360
394
|
}
|
|
361
395
|
|
|
396
|
+
// ─── Layer 3: Structural Schema Validation ───────────────────────────────────
|
|
397
|
+
|
|
398
|
+
const KNOWN_VALID_TAGS = new Set([
|
|
399
|
+
'objective', 'process', 'step', 'success_criteria', 'critical_rules',
|
|
400
|
+
'available_agent_types', 'purpose', 'required_reading',
|
|
401
|
+
]);
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Validate the XML structure of a prompt file.
|
|
405
|
+
* For agent/workflow files, flags any XML tag not in the known-valid set.
|
|
406
|
+
*
|
|
407
|
+
* @param {string} text - The file content to validate
|
|
408
|
+
* @param {'agent'|'workflow'|'unknown'} fileType - The type of prompt file
|
|
409
|
+
* @returns {{ valid: boolean, violations: string[] }}
|
|
410
|
+
*/
|
|
411
|
+
function validatePromptStructure(text, fileType) {
|
|
412
|
+
if (!text || typeof text !== 'string') {
|
|
413
|
+
return { valid: true, violations: [] };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (fileType !== 'agent' && fileType !== 'workflow') {
|
|
417
|
+
return { valid: true, violations: [] };
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const violations = [];
|
|
421
|
+
const tagRegex = /<([A-Za-z][A-Za-z0-9_-]*)/g;
|
|
422
|
+
let match;
|
|
423
|
+
while ((match = tagRegex.exec(text)) !== null) {
|
|
424
|
+
const tag = match[1].toLowerCase();
|
|
425
|
+
if (!KNOWN_VALID_TAGS.has(tag)) {
|
|
426
|
+
violations.push(`Unknown XML tag in ${fileType} file: <${tag}>`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return { valid: violations.length === 0, violations };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ─── Layer 4: Paragraph-Level Entropy Anomaly Detection ─────────────────────
|
|
434
|
+
|
|
435
|
+
function shannonEntropy(text) {
|
|
436
|
+
if (!text || text.length === 0) return 0;
|
|
437
|
+
const freq = {};
|
|
438
|
+
for (const ch of text) {
|
|
439
|
+
freq[ch] = (freq[ch] || 0) + 1;
|
|
440
|
+
}
|
|
441
|
+
const len = text.length;
|
|
442
|
+
let entropy = 0;
|
|
443
|
+
for (const count of Object.values(freq)) {
|
|
444
|
+
const p = count / len;
|
|
445
|
+
entropy -= p * Math.log2(p);
|
|
446
|
+
}
|
|
447
|
+
return entropy;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Scan text for paragraphs with anomalously high Shannon entropy.
|
|
452
|
+
*
|
|
453
|
+
* @param {string} text - The text to scan
|
|
454
|
+
* @returns {{ clean: boolean, findings: string[] }}
|
|
455
|
+
*/
|
|
456
|
+
function scanEntropyAnomalies(text) {
|
|
457
|
+
if (!text || typeof text !== 'string') {
|
|
458
|
+
return { clean: true, findings: [] };
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const findings = [];
|
|
462
|
+
const paragraphs = text.split(/\n\n+/);
|
|
463
|
+
|
|
464
|
+
for (const para of paragraphs) {
|
|
465
|
+
if (para.length <= 50) continue;
|
|
466
|
+
const entropy = shannonEntropy(para);
|
|
467
|
+
if (entropy > 5.5) {
|
|
468
|
+
findings.push(
|
|
469
|
+
`High-entropy paragraph detected (${entropy.toFixed(2)} bits/char) — possible encoded payload`
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return { clean: findings.length === 0, findings };
|
|
475
|
+
}
|
|
476
|
+
|
|
362
477
|
module.exports = {
|
|
363
478
|
// Path safety
|
|
364
479
|
validatePath,
|
|
@@ -379,4 +494,10 @@ module.exports = {
|
|
|
379
494
|
// Input validation
|
|
380
495
|
validatePhaseNumber,
|
|
381
496
|
validateFieldName,
|
|
497
|
+
|
|
498
|
+
// Structural validation (Layer 3)
|
|
499
|
+
validatePromptStructure,
|
|
500
|
+
|
|
501
|
+
// Entropy anomaly detection (Layer 4)
|
|
502
|
+
scanEntropyAnomalies,
|
|
382
503
|
};
|