swarm-orchestrator 7.0.0-alpha.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/LICENSE +15 -0
- package/README.md +199 -0
- package/agents/reviewer.agent.md +50 -0
- package/agents/worker.agent.md +41 -0
- package/config/default-agents.yaml +56 -0
- package/config/pm-agent.yaml +36 -0
- package/config/quality-gates.yaml +88 -0
- package/config/repair-agent.yaml +34 -0
- package/config/semgrep-rules/complexity-mismatch.yaml +6 -0
- package/config/semgrep-rules/exception-swallowing.yaml +13 -0
- package/config/semgrep-rules/hardcoded-answer.yaml +6 -0
- package/config/semgrep-rules/mock-mutation.yaml +6 -0
- package/config/semgrep-rules/test-modification.yaml +6 -0
- package/config/user-agents.yaml +23 -0
- package/dist/src/adapters/adapter-factory.js +29 -0
- package/dist/src/adapters/agent-adapter.js +32 -0
- package/dist/src/adapters/claude-code-adapter.js +126 -0
- package/dist/src/adapters/claude-code-teams.js +119 -0
- package/dist/src/adapters/codex-adapter.js +118 -0
- package/dist/src/adapters/copilot-adapter.js +224 -0
- package/dist/src/adapters/fatal-error-classifier.js +88 -0
- package/dist/src/adapters/index.js +44 -0
- package/dist/src/adapters/persistent-session.js +204 -0
- package/dist/src/adapters/process-supervisor.js +199 -0
- package/dist/src/agents-exporter.js +398 -0
- package/dist/src/analytics-log.js +161 -0
- package/dist/src/baseline-scanner.js +65 -0
- package/dist/src/bootstrap-evidence.js +88 -0
- package/dist/src/bootstrap-orchestrator.js +205 -0
- package/dist/src/bootstrap-types.js +5 -0
- package/dist/src/branch-merger.js +427 -0
- package/dist/src/cli/attest-handlers.js +44 -0
- package/dist/src/cli/cost-prompt.js +51 -0
- package/dist/src/cli/demo-handlers.js +274 -0
- package/dist/src/cli/flags.js +152 -0
- package/dist/src/cli/index.js +57 -0
- package/dist/src/cli/live-status.js +186 -0
- package/dist/src/cli/misc-handlers.js +242 -0
- package/dist/src/cli/plan-handlers.js +350 -0
- package/dist/src/cli/share-handlers.js +163 -0
- package/dist/src/cli/status-handlers.js +457 -0
- package/dist/src/cli/swarm-handlers.js +638 -0
- package/dist/src/cli/usage.js +72 -0
- package/dist/src/cli.js +217 -0
- package/dist/src/commit-pattern-detector.js +143 -0
- package/dist/src/commit-quality-analyzer.js +38 -0
- package/dist/src/config-loader.js +339 -0
- package/dist/src/context-broker.js +358 -0
- package/dist/src/copilot-cli-wrapper.js +258 -0
- package/dist/src/cost-estimator.js +241 -0
- package/dist/src/defaults.js +18 -0
- package/dist/src/demo-mode.js +180 -0
- package/dist/src/deployment-handler.js +78 -0
- package/dist/src/deployment-manager.js +228 -0
- package/dist/src/execution-queue.js +196 -0
- package/dist/src/external-tool-manager.js +184 -0
- package/dist/src/gate-prompt-builder.js +117 -0
- package/dist/src/gate-remediation.js +173 -0
- package/dist/src/github-issues-ingester.js +152 -0
- package/dist/src/github-mcp-integrator.js +164 -0
- package/dist/src/hook-generator.js +229 -0
- package/dist/src/index.js +48 -0
- package/dist/src/knowledge-base.js +300 -0
- package/dist/src/logger.js +111 -0
- package/dist/src/meta-analyzer.js +208 -0
- package/dist/src/metrics-collector.js +209 -0
- package/dist/src/metrics-types.js +5 -0
- package/dist/src/multi-repo-coordinator.js +179 -0
- package/dist/src/orchestrator/async-meta-analysis.js +84 -0
- package/dist/src/orchestrator/fatal-run-error.js +43 -0
- package/dist/src/orchestrator/final-gates-remediation.js +258 -0
- package/dist/src/orchestrator/git-state-utils.js +119 -0
- package/dist/src/orchestrator/pause-controller.js +55 -0
- package/dist/src/orchestrator/replan-runner.js +258 -0
- package/dist/src/orchestrator/step-executor.js +391 -0
- package/dist/src/orchestrator/wave-scheduler-loop.js +204 -0
- package/dist/src/owasp-mapper.js +153 -0
- package/dist/src/owasp-report-renderer.js +39 -0
- package/dist/src/plan-files.js +142 -0
- package/dist/src/plan-generator.js +1207 -0
- package/dist/src/pm-agent.js +296 -0
- package/dist/src/post-run-reporter.js +203 -0
- package/dist/src/pr-automation.js +103 -0
- package/dist/src/pr-manager.js +318 -0
- package/dist/src/presenter/index.js +209 -0
- package/dist/src/prompt-builder.js +173 -0
- package/dist/src/quality-gates/config-loader.js +196 -0
- package/dist/src/quality-gates/default-config.js +243 -0
- package/dist/src/quality-gates/file-utils.js +152 -0
- package/dist/src/quality-gates/gate-runner.js +171 -0
- package/dist/src/quality-gates/gates/accessibility.js +355 -0
- package/dist/src/quality-gates/gates/duplicate-blocks.js +144 -0
- package/dist/src/quality-gates/gates/hardcoded-config.js +52 -0
- package/dist/src/quality-gates/gates/readme-claims.js +62 -0
- package/dist/src/quality-gates/gates/runtime-checks.js +265 -0
- package/dist/src/quality-gates/gates/scaffold-defaults.js +108 -0
- package/dist/src/quality-gates/gates/test-coverage.js +240 -0
- package/dist/src/quality-gates/gates/test-file-protection.js +85 -0
- package/dist/src/quality-gates/gates/test-isolation.js +74 -0
- package/dist/src/quality-gates/index.js +17 -0
- package/dist/src/quality-gates/registry.js +205 -0
- package/dist/src/quality-gates/types.js +2 -0
- package/dist/src/quick-fix-mode.js +302 -0
- package/dist/src/recipe-loader.js +122 -0
- package/dist/src/repair-agent.js +396 -0
- package/dist/src/repo-analyzer.js +342 -0
- package/dist/src/report-generator.js +146 -0
- package/dist/src/report-renderer.js +90 -0
- package/dist/src/requirement-filter.js +52 -0
- package/dist/src/sarif-formatter.js +140 -0
- package/dist/src/scheduling/dependency-analyzer.js +178 -0
- package/dist/src/scheduling/work-stealing-queue.js +123 -0
- package/dist/src/secret-redactor.js +90 -0
- package/dist/src/session-executor.js +587 -0
- package/dist/src/session-manager.js +221 -0
- package/dist/src/share/transcript-verification.js +128 -0
- package/dist/src/share-parser.js +477 -0
- package/dist/src/spinner.js +146 -0
- package/dist/src/step-runner.js +365 -0
- package/dist/src/swarm-orchestrator.js +686 -0
- package/dist/src/task-classifier.js +134 -0
- package/dist/src/test-command-discovery.js +123 -0
- package/dist/src/text-similarity.js +24 -0
- package/dist/src/tier-maps.js +158 -0
- package/dist/src/types.js +5 -0
- package/dist/src/url-shortener.js +135 -0
- package/dist/src/verification/attestation.js +166 -0
- package/dist/src/verification/cheat-detector.js +240 -0
- package/dist/src/verification/command-runner.js +70 -0
- package/dist/src/verification/composite-score.js +157 -0
- package/dist/src/verification/cosign-attestation.js +137 -0
- package/dist/src/verification/diff-analysis.js +89 -0
- package/dist/src/verification/differential-gate.js +173 -0
- package/dist/src/verification/index.js +41 -0
- package/dist/src/verification/mutation-gate.js +242 -0
- package/dist/src/verification/property-gate.js +201 -0
- package/dist/src/verification/test-synthesizer.js +243 -0
- package/dist/src/verifier/outcome-checks.js +396 -0
- package/dist/src/verifier/transcript-checks.js +207 -0
- package/dist/src/verifier/verification-reporters.js +148 -0
- package/dist/src/verifier-engine.js +491 -0
- package/dist/src/wave-resizer.js +131 -0
- package/dist/src/wave-scheduler.js +44 -0
- package/dist/src/worktree-manager.js +368 -0
- package/dist/src/worktree-reserved-paths.js +97 -0
- package/package.json +80 -0
- package/plugin/agents/.gitkeep +0 -0
- package/plugin/agents/backend_master.agent.md +72 -0
- package/plugin/agents/devops_pro.agent.md +45 -0
- package/plugin/agents/frontend_expert.agent.md +59 -0
- package/plugin/agents/integrator_finalizer.agent.md +44 -0
- package/plugin/agents/meta_reviewer.agent.md +44 -0
- package/plugin/agents/security_auditor.agent.md +43 -0
- package/plugin/agents/tester_elite.agent.md +43 -0
- package/plugin/hooks/hooks.json +19 -0
- package/plugin/hooks/post-tool-use.json +9 -0
- package/plugin/hooks/pre-tool-use.json +9 -0
- package/plugin/plugin.json +47 -0
- package/plugin/skills/swarm-gates/SKILL.md +53 -0
- package/plugin/skills/swarm-orchestrate/SKILL.md +34 -0
- package/plugin/skills/swarm-verify/SKILL.md +44 -0
- package/templates/README.md +35 -0
- package/templates/cli-tool.json +31 -0
- package/templates/fullstack.json +52 -0
- package/templates/library.json +38 -0
- package/templates/react-app.json +45 -0
- package/templates/recipes/add-api-docs.json +35 -0
- package/templates/recipes/add-auth.json +47 -0
- package/templates/recipes/add-ci.json +39 -0
- package/templates/recipes/add-tests.json +39 -0
- package/templates/recipes/migrate-to-ts.json +42 -0
- package/templates/recipes/refactor-modularize.json +44 -0
- package/templates/recipes/security-audit.json +35 -0
- package/templates/rest-api.json +38 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Deterministic goal-to-task-type classifier using keyword matching.
|
|
3
|
+
// No LLM call: classification drives tier-based requirement filtering
|
|
4
|
+
// so it must be fast, reproducible, and testable.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TaskClassifier = void 0;
|
|
7
|
+
// Each pattern is a regex that matches whole-ish tokens in the goal prompt.
|
|
8
|
+
// Word boundaries prevent "express" from matching inside "expression".
|
|
9
|
+
const CATEGORY_PATTERNS = {
|
|
10
|
+
'api-backend': [
|
|
11
|
+
/\bexpress\b/i,
|
|
12
|
+
/\bfastapi\b/i,
|
|
13
|
+
/\bflask\b/i,
|
|
14
|
+
/\brest\b/i,
|
|
15
|
+
/\bapi\b/i,
|
|
16
|
+
/\bendpoints?\b/i,
|
|
17
|
+
/\broutes?\b/i,
|
|
18
|
+
/\bmiddleware\b/i,
|
|
19
|
+
/\bserver\b/i,
|
|
20
|
+
/\bbackend\b/i,
|
|
21
|
+
/\bdatabase\b/i,
|
|
22
|
+
/\b(?:GET|POST|PUT|DELETE|PATCH)\b/,
|
|
23
|
+
/\bport\b/i,
|
|
24
|
+
/\bcors\b/i,
|
|
25
|
+
],
|
|
26
|
+
'frontend-web': [
|
|
27
|
+
/\bhtml\b/i,
|
|
28
|
+
/\bcss\b/i,
|
|
29
|
+
/\bbrowser[- ]based\b/i,
|
|
30
|
+
/\breact\b/i,
|
|
31
|
+
/\bvue\b/i,
|
|
32
|
+
/\bsvelte\b/i,
|
|
33
|
+
/\bangular\b/i,
|
|
34
|
+
/\bdashboard\b/i,
|
|
35
|
+
/\bpanel\b/i,
|
|
36
|
+
/\bcomponents?\b/i,
|
|
37
|
+
/\bresponsive\b/i,
|
|
38
|
+
/\baccessibility\b/i,
|
|
39
|
+
/\baria\b/i,
|
|
40
|
+
/\bdom\b/i,
|
|
41
|
+
/\bcanvas\b/i,
|
|
42
|
+
/\bsvg\b/i,
|
|
43
|
+
/\bui\b/i,
|
|
44
|
+
/\bfrontend\b/i,
|
|
45
|
+
/\bwebapp\b/i,
|
|
46
|
+
/\bweb\s+app\b/i,
|
|
47
|
+
/\blanding\s+page\b/i,
|
|
48
|
+
/\bforms?\b/i,
|
|
49
|
+
/\bmodal\b/i,
|
|
50
|
+
],
|
|
51
|
+
'cli-tool': [
|
|
52
|
+
/\bcli\b/i,
|
|
53
|
+
/\bcommand\s+line\b/i,
|
|
54
|
+
/\bterminal\b/i,
|
|
55
|
+
/\bflags?\b/i,
|
|
56
|
+
/--\w+/,
|
|
57
|
+
/\barguments?\b/i,
|
|
58
|
+
/\bargv\b/i,
|
|
59
|
+
/\bstdin\b/i,
|
|
60
|
+
/\bstdout\b/i,
|
|
61
|
+
/\bpipes?\b/i,
|
|
62
|
+
/\bshell\b/i,
|
|
63
|
+
/\bbinary\b/i,
|
|
64
|
+
/\bbin\b/i,
|
|
65
|
+
/\bsubcommands?\b/i,
|
|
66
|
+
],
|
|
67
|
+
'library-package': [
|
|
68
|
+
/\blibrary\b/i,
|
|
69
|
+
/\bpackage\b/i,
|
|
70
|
+
/\bsdk\b/i,
|
|
71
|
+
/\bnpm\s+package\b/i,
|
|
72
|
+
/\bpip\s+package\b/i,
|
|
73
|
+
/\bcrate\b/i,
|
|
74
|
+
/\bimportable\b/i,
|
|
75
|
+
/\breusable\b/i,
|
|
76
|
+
/\bpublishable\b/i,
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
function matchCategory(prompt, category) {
|
|
80
|
+
const matched = [];
|
|
81
|
+
for (const pattern of CATEGORY_PATTERNS[category]) {
|
|
82
|
+
const match = prompt.match(pattern);
|
|
83
|
+
if (match) {
|
|
84
|
+
matched.push(match[0]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { category, keywords: matched, count: matched.length };
|
|
88
|
+
}
|
|
89
|
+
class TaskClassifier {
|
|
90
|
+
classify(goalPrompt) {
|
|
91
|
+
if (!goalPrompt || goalPrompt.trim().length === 0) {
|
|
92
|
+
return { taskType: 'full-stack', confidence: 'low', matchedKeywords: [] };
|
|
93
|
+
}
|
|
94
|
+
const categories = [
|
|
95
|
+
'api-backend', 'frontend-web', 'cli-tool', 'library-package'
|
|
96
|
+
];
|
|
97
|
+
const matches = categories.map(cat => matchCategory(goalPrompt, cat));
|
|
98
|
+
const backendMatch = matches.find(m => m.category === 'api-backend');
|
|
99
|
+
const frontendMatch = matches.find(m => m.category === 'frontend-web');
|
|
100
|
+
// Full-stack: both backend and frontend have 2+ matches
|
|
101
|
+
if (backendMatch.count >= 2 && frontendMatch.count >= 2) {
|
|
102
|
+
const combined = [...backendMatch.keywords, ...frontendMatch.keywords];
|
|
103
|
+
return { taskType: 'full-stack', confidence: 'high', matchedKeywords: combined };
|
|
104
|
+
}
|
|
105
|
+
// Sort by match count descending
|
|
106
|
+
const sorted = [...matches].sort((a, b) => b.count - a.count);
|
|
107
|
+
const winner = sorted[0];
|
|
108
|
+
const runner = sorted[1];
|
|
109
|
+
// No category has 2+ matches: default to full-stack with low confidence
|
|
110
|
+
if (winner.count < 2) {
|
|
111
|
+
if (winner.count === 1) {
|
|
112
|
+
return { taskType: 'full-stack', confidence: 'low', matchedKeywords: winner.keywords };
|
|
113
|
+
}
|
|
114
|
+
return { taskType: 'full-stack', confidence: 'low', matchedKeywords: [] };
|
|
115
|
+
}
|
|
116
|
+
// Determine confidence based on separation from runner-up
|
|
117
|
+
let confidence;
|
|
118
|
+
if (winner.count >= 3 && runner.count < 2) {
|
|
119
|
+
confidence = 'high';
|
|
120
|
+
}
|
|
121
|
+
else if (winner.count >= 2) {
|
|
122
|
+
confidence = 'medium';
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
confidence = 'low';
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
taskType: winner.category,
|
|
129
|
+
confidence,
|
|
130
|
+
matchedKeywords: winner.keywords,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
exports.TaskClassifier = TaskClassifier;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.discoverTestCommand = discoverTestCommand;
|
|
37
|
+
exports.renderVerifyCommandSection = renderVerifyCommandSection;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const path = __importStar(require("path"));
|
|
40
|
+
/**
|
|
41
|
+
* Discover the target project's full test command from its package.json.
|
|
42
|
+
*
|
|
43
|
+
* Reads `scripts.test` and combines it with the detected package manager
|
|
44
|
+
* (from pnpm-lock.yaml, yarn.lock, or package-lock.json) to produce the
|
|
45
|
+
* single command agents should run to verify their work.
|
|
46
|
+
*
|
|
47
|
+
* @param projectRoot absolute path to the target project
|
|
48
|
+
* @returns discovery result including the command and any fallback warning
|
|
49
|
+
*/
|
|
50
|
+
function discoverTestCommand(projectRoot) {
|
|
51
|
+
const packageManager = detectPackageManager(projectRoot);
|
|
52
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
53
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
54
|
+
return {
|
|
55
|
+
command: `${packageManager} test`,
|
|
56
|
+
rawScript: undefined,
|
|
57
|
+
hasScript: false,
|
|
58
|
+
packageManager,
|
|
59
|
+
warning: `no package.json found at ${packageJsonPath}; falling back to "${packageManager} test"`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
let pkg;
|
|
63
|
+
try {
|
|
64
|
+
pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return {
|
|
68
|
+
command: `${packageManager} test`,
|
|
69
|
+
rawScript: undefined,
|
|
70
|
+
hasScript: false,
|
|
71
|
+
packageManager,
|
|
72
|
+
warning: `failed to parse ${packageJsonPath}: ${err.message}; falling back to "${packageManager} test"`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const rawScript = pkg.scripts?.test;
|
|
76
|
+
if (!rawScript || rawScript.trim() === '') {
|
|
77
|
+
return {
|
|
78
|
+
command: `${packageManager} test`,
|
|
79
|
+
rawScript: undefined,
|
|
80
|
+
hasScript: false,
|
|
81
|
+
packageManager,
|
|
82
|
+
warning: `no "test" script in ${packageJsonPath}; falling back to "${packageManager} test" (may fail if no test setup exists)`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
command: `${packageManager} test`,
|
|
87
|
+
rawScript,
|
|
88
|
+
hasScript: true,
|
|
89
|
+
packageManager,
|
|
90
|
+
warning: undefined,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function detectPackageManager(projectRoot) {
|
|
94
|
+
if (fs.existsSync(path.join(projectRoot, 'pnpm-lock.yaml')))
|
|
95
|
+
return 'pnpm';
|
|
96
|
+
if (fs.existsSync(path.join(projectRoot, 'yarn.lock')))
|
|
97
|
+
return 'yarn';
|
|
98
|
+
return 'npm';
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Render the prompt section that tells an agent how to verify its work.
|
|
102
|
+
*
|
|
103
|
+
* Every prompt-building path in the orchestrator must include this block so
|
|
104
|
+
* agents run the project's full test gate (lint + tests + types) rather
|
|
105
|
+
* than a subset runner like `npx vitest --run` that would skip lint/types.
|
|
106
|
+
*/
|
|
107
|
+
function renderVerifyCommandSection(discovery) {
|
|
108
|
+
const lines = [];
|
|
109
|
+
lines.push('Verify before committing (REQUIRED)');
|
|
110
|
+
lines.push('-----------------------------------');
|
|
111
|
+
lines.push(`Before committing, run \`${discovery.command}\` and verify it passes.`);
|
|
112
|
+
lines.push('Do not run individual test tools directly. Run the project\'s full test script.');
|
|
113
|
+
if (discovery.hasScript && discovery.rawScript) {
|
|
114
|
+
lines.push(`This project's "${discovery.command}" runs: ${discovery.rawScript}`);
|
|
115
|
+
lines.push('Running a subset (e.g. `npx vitest --run`) will miss lint and type checks and your commit may fail.');
|
|
116
|
+
}
|
|
117
|
+
else if (discovery.warning) {
|
|
118
|
+
lines.push(`WARNING: ${discovery.warning}`);
|
|
119
|
+
lines.push('If no real test gate exists, say so in your verification section rather than fabricating output.');
|
|
120
|
+
}
|
|
121
|
+
lines.push('');
|
|
122
|
+
return lines.join('\n');
|
|
123
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared text similarity utilities.
|
|
4
|
+
* Extracted from knowledge-base.ts to eliminate the
|
|
5
|
+
* duplicate Levenshtein implementation.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.levenshtein = levenshtein;
|
|
9
|
+
/**
|
|
10
|
+
* Standard Levenshtein distance between two strings.
|
|
11
|
+
* O(m*n) time and space where m and n are string lengths.
|
|
12
|
+
*/
|
|
13
|
+
function levenshtein(a, b) {
|
|
14
|
+
const m = a.length, n = b.length;
|
|
15
|
+
const dp = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
|
|
16
|
+
for (let i = 1; i <= m; i++) {
|
|
17
|
+
for (let j = 1; j <= n; j++) {
|
|
18
|
+
dp[i][j] = a[i - 1] === b[j - 1]
|
|
19
|
+
? dp[i - 1][j - 1]
|
|
20
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return dp[m][n];
|
|
24
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Tier maps: per-task-type requirement classifications.
|
|
3
|
+
// Pure data, no logic. Consumed by RequirementFilter to decide
|
|
4
|
+
// which requirements get injected, recommended, or suppressed.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TIER_MAPS = void 0;
|
|
7
|
+
const API_BACKEND_ENFORCE = [
|
|
8
|
+
{ id: 'security-headers', description: 'Security headers on HTTP responses (X-Content-Type-Options, X-Frame-Options, CSP minimum)', tier: 'enforce', category: 'security' },
|
|
9
|
+
{ id: 'body-size-limit', description: 'Global request body size limit via middleware (e.g. express.json({ limit }), Starlette ContentSizeLimitMiddleware), not per-field max_length', tier: 'enforce', category: 'security' },
|
|
10
|
+
{ id: 'param-sanitization', description: 'ID and route parameter validation before route logic (regex or schema)', tier: 'enforce', category: 'validation' },
|
|
11
|
+
{ id: 'input-type-validation', description: 'Type checks on all input fields (reject non-string where string expected, etc.)', tier: 'enforce', category: 'validation' },
|
|
12
|
+
{ id: 'input-length-validation', description: 'Max length enforcement on string inputs', tier: 'enforce', category: 'validation' },
|
|
13
|
+
{ id: 'input-empty-validation', description: 'Empty and whitespace-only rejection on required fields', tier: 'enforce', category: 'validation' },
|
|
14
|
+
{ id: 'error-hide-internals', description: 'Error handler that never leaks err.message or stack traces to clients', tier: 'enforce', category: 'security' },
|
|
15
|
+
{ id: 'config-externalization', description: 'Environment-dependent values (PORT, DB paths, CORS origins, file paths) from env vars, not hardcoded', tier: 'enforce', category: 'structure' },
|
|
16
|
+
{ id: 'endpoint-tests', description: 'Tests for every endpoint covering happy path and at least one error path', tier: 'enforce', category: 'testing' },
|
|
17
|
+
{ id: 'validation-tests', description: 'Tests for every validation rule: positive (valid input accepted) and negative (invalid input rejected with correct status code and error shape) for each type check, length check, empty check, and schema constraint', tier: 'enforce', category: 'testing' },
|
|
18
|
+
{ id: 'gitignore', description: '.gitignore present with node_modules, dist, .env at minimum', tier: 'enforce', category: 'structure' },
|
|
19
|
+
{ id: 'package-json-complete', description: 'package.json with name, description, scripts (start, test, dev), engines', tier: 'enforce', category: 'structure' },
|
|
20
|
+
];
|
|
21
|
+
const API_BACKEND_RECOMMEND = [
|
|
22
|
+
{ id: 'readme-api-docs', description: 'README with endpoint documentation and env var reference', tier: 'recommend', category: 'documentation' },
|
|
23
|
+
{ id: 'dockerfile', description: 'Dockerfile for containerized deployment', tier: 'recommend', category: 'structure' },
|
|
24
|
+
{ id: 'rate-limiting', description: 'Rate limiting middleware or configuration', tier: 'recommend', category: 'security' },
|
|
25
|
+
{ id: 'health-endpoint-timestamp', description: 'Health endpoint returns a timestamp, not just { status: "ok" }', tier: 'recommend', category: 'resilience' },
|
|
26
|
+
{ id: '404-unknown-routes', description: 'Catch-all handler for undefined routes returning JSON error', tier: 'recommend', category: 'resilience' },
|
|
27
|
+
{ id: 'test-isolation', description: 'Tests use temp directories with cleanup, not shared state', tier: 'recommend', category: 'testing' },
|
|
28
|
+
{ id: 'delete-returns-confirmation', description: 'DELETE endpoints return the deleted object or count, not empty 204', tier: 'recommend', category: 'structure' },
|
|
29
|
+
];
|
|
30
|
+
const API_BACKEND_SKIP = [
|
|
31
|
+
{ id: 'audio-feedback', description: 'Any audio/sound features', tier: 'skip', category: 'polish' },
|
|
32
|
+
{ id: 'favicon', description: 'Custom favicons', tier: 'skip', category: 'polish' },
|
|
33
|
+
{ id: 'theme-color-meta', description: 'Theme color meta tags', tier: 'skip', category: 'polish' },
|
|
34
|
+
{ id: 'css-custom-properties', description: 'Elaborate CSS variable systems', tier: 'skip', category: 'polish' },
|
|
35
|
+
{ id: 'aria-attributes', description: 'ARIA labels (not relevant for APIs)', tier: 'skip', category: 'accessibility' },
|
|
36
|
+
{ id: 'responsive-layout', description: 'Responsive CSS (not relevant for APIs)', tier: 'skip', category: 'accessibility' },
|
|
37
|
+
{ id: 'skip-link', description: 'Skip navigation links (not relevant for APIs)', tier: 'skip', category: 'accessibility' },
|
|
38
|
+
{ id: 'dark-mode', description: 'Color scheme support (not relevant for APIs)', tier: 'skip', category: 'polish' },
|
|
39
|
+
];
|
|
40
|
+
const FRONTEND_WEB_ENFORCE = [
|
|
41
|
+
{ id: 'aria-interactive', description: 'ARIA labels on all interactive elements (buttons, inputs, links, toggles)', tier: 'enforce', category: 'accessibility' },
|
|
42
|
+
{ id: 'keyboard-navigation', description: 'All interactive elements reachable and operable via keyboard', tier: 'enforce', category: 'accessibility' },
|
|
43
|
+
{ id: 'focus-visible', description: ':focus-visible styles on all interactive elements', tier: 'enforce', category: 'accessibility' },
|
|
44
|
+
{ id: 'semantic-html', description: 'Semantic landmarks (main, nav, header, section, article as appropriate)', tier: 'enforce', category: 'accessibility' },
|
|
45
|
+
{ id: 'responsive-breakpoint', description: 'At least one responsive breakpoint for mobile', tier: 'enforce', category: 'accessibility' },
|
|
46
|
+
{ id: 'error-states', description: 'Error state UI for failed operations (not just happy path)', tier: 'enforce', category: 'resilience' },
|
|
47
|
+
{ id: 'loading-states', description: 'Loading indicators for async operations', tier: 'enforce', category: 'resilience' },
|
|
48
|
+
{ id: 'empty-states', description: 'Empty state UI when no data exists', tier: 'enforce', category: 'resilience' },
|
|
49
|
+
{ id: 'input-validation-frontend', description: 'Client-side validation on form inputs before submission', tier: 'enforce', category: 'validation' },
|
|
50
|
+
{ id: 'component-tests', description: 'Tests for core UI logic (state changes, event handlers, data transformations)', tier: 'enforce', category: 'testing' },
|
|
51
|
+
{ id: 'gitignore', description: '.gitignore present', tier: 'enforce', category: 'structure' },
|
|
52
|
+
{ id: 'package-json-complete', description: 'package.json with name, description, scripts', tier: 'enforce', category: 'structure' },
|
|
53
|
+
];
|
|
54
|
+
const FRONTEND_WEB_RECOMMEND = [
|
|
55
|
+
{ id: 'prefers-color-scheme', description: 'Dark mode via prefers-color-scheme media query', tier: 'recommend', category: 'accessibility' },
|
|
56
|
+
{ id: 'prefers-reduced-motion', description: 'Reduced motion support for animations', tier: 'recommend', category: 'accessibility' },
|
|
57
|
+
{ id: 'skip-link', description: 'Skip navigation link for screen readers', tier: 'recommend', category: 'accessibility' },
|
|
58
|
+
{ id: 'meta-description', description: 'Meta description tag', tier: 'recommend', category: 'documentation' },
|
|
59
|
+
{ id: 'readme', description: 'README with usage instructions', tier: 'recommend', category: 'documentation' },
|
|
60
|
+
{ id: 'css-custom-properties', description: 'CSS custom properties for theming (colors, spacing at minimum)', tier: 'recommend', category: 'structure' },
|
|
61
|
+
{ id: 'zero-dependencies', description: 'Prefer custom implementations over CDN dependencies where reasonable', tier: 'recommend', category: 'structure' },
|
|
62
|
+
];
|
|
63
|
+
const FRONTEND_WEB_SKIP = [
|
|
64
|
+
{ id: 'audio-feedback', description: 'Web Audio API sounds', tier: 'skip', category: 'polish' },
|
|
65
|
+
{ id: 'favicon-custom', description: 'Custom inline SVG favicon', tier: 'skip', category: 'polish' },
|
|
66
|
+
{ id: 'theme-color-meta', description: 'Dual theme-color meta tags', tier: 'skip', category: 'polish' },
|
|
67
|
+
{ id: 'elaborate-animations', description: 'Complex animation systems', tier: 'skip', category: 'polish' },
|
|
68
|
+
{ id: 'security-headers', description: 'Security headers (not relevant for static frontends without a server)', tier: 'skip', category: 'security' },
|
|
69
|
+
];
|
|
70
|
+
const CLI_TOOL_ENFORCE = [
|
|
71
|
+
{ id: 'arg-validation', description: 'Argument validation with clear, specific error messages including the invalid value', tier: 'enforce', category: 'validation' },
|
|
72
|
+
{ id: 'help-flag', description: '--help / -h flag with usage documentation', tier: 'enforce', category: 'documentation' },
|
|
73
|
+
{ id: 'missing-file-error', description: 'Graceful handling of missing files with specific path in error message', tier: 'enforce', category: 'resilience' },
|
|
74
|
+
{ id: 'permission-error', description: 'Graceful handling of permission errors with specific message', tier: 'enforce', category: 'resilience' },
|
|
75
|
+
{ id: 'exit-codes', description: 'Non-zero exit codes on errors (0 success, 1 user error, 2 system error)', tier: 'enforce', category: 'structure' },
|
|
76
|
+
{ id: 'core-logic-tests', description: 'Tests for all core logic functions (parsers, transformers, validators)', tier: 'enforce', category: 'testing' },
|
|
77
|
+
{ id: 'error-path-tests', description: 'Tests for all error conditions (missing file, bad input, permission denied)', tier: 'enforce', category: 'testing' },
|
|
78
|
+
{ id: 'gitignore', description: '.gitignore present', tier: 'enforce', category: 'structure' },
|
|
79
|
+
{ id: 'package-json-complete', description: 'package.json with name, description, bin field, engines', tier: 'enforce', category: 'structure' },
|
|
80
|
+
];
|
|
81
|
+
const CLI_TOOL_RECOMMEND = [
|
|
82
|
+
{ id: 'short-flags', description: 'Short flag aliases (-h, -v, etc.) in addition to long forms', tier: 'recommend', category: 'structure' },
|
|
83
|
+
{ id: 'readme-usage', description: 'README with usage examples and flag documentation', tier: 'recommend', category: 'documentation' },
|
|
84
|
+
{ id: 'no-color-support', description: 'NO_COLOR env var and TTY detection for colored output', tier: 'recommend', category: 'resilience' },
|
|
85
|
+
{ id: 'stdin-support', description: 'Read from stdin when no file argument provided (where applicable)', tier: 'recommend', category: 'structure' },
|
|
86
|
+
{ id: 'config-env-vars', description: 'Configuration via environment variables where appropriate', tier: 'recommend', category: 'structure' },
|
|
87
|
+
];
|
|
88
|
+
const CLI_TOOL_SKIP = [
|
|
89
|
+
{ id: 'aria-attributes', description: 'ARIA (not relevant)', tier: 'skip', category: 'accessibility' },
|
|
90
|
+
{ id: 'responsive-layout', description: 'Responsive CSS (not relevant)', tier: 'skip', category: 'accessibility' },
|
|
91
|
+
{ id: 'dark-mode', description: 'Browser dark mode (not relevant)', tier: 'skip', category: 'polish' },
|
|
92
|
+
{ id: 'favicon', description: 'Favicons (not relevant)', tier: 'skip', category: 'polish' },
|
|
93
|
+
{ id: 'audio-feedback', description: 'Audio (not relevant)', tier: 'skip', category: 'polish' },
|
|
94
|
+
{ id: 'security-headers', description: 'HTTP security headers (not relevant)', tier: 'skip', category: 'security' },
|
|
95
|
+
{ id: 'css-custom-properties', description: 'CSS variables (not relevant)', tier: 'skip', category: 'polish' },
|
|
96
|
+
];
|
|
97
|
+
const LIBRARY_PACKAGE_ENFORCE = [
|
|
98
|
+
{ id: 'type-exports', description: 'All public API types exported and documented', tier: 'enforce', category: 'structure' },
|
|
99
|
+
{ id: 'input-validation', description: 'Validate public function inputs with descriptive errors', tier: 'enforce', category: 'validation' },
|
|
100
|
+
{ id: 'error-messages-specific', description: 'Error messages include the invalid value and expected format', tier: 'enforce', category: 'validation' },
|
|
101
|
+
{ id: 'unit-tests-core', description: 'Unit tests for all exported functions covering happy path, edge cases, error conditions', tier: 'enforce', category: 'testing' },
|
|
102
|
+
{ id: 'zero-side-effects', description: 'No side effects in library code (no console.log, no file writes, no network calls)', tier: 'enforce', category: 'structure' },
|
|
103
|
+
{ id: 'gitignore', description: '.gitignore present', tier: 'enforce', category: 'structure' },
|
|
104
|
+
{ id: 'package-json-complete', description: 'package.json with name, description, main/exports, types, engines, license', tier: 'enforce', category: 'structure' },
|
|
105
|
+
];
|
|
106
|
+
const LIBRARY_PACKAGE_RECOMMEND = [
|
|
107
|
+
{ id: 'readme-api', description: 'README with API documentation and usage examples', tier: 'recommend', category: 'documentation' },
|
|
108
|
+
{ id: 'jsdoc-public', description: 'JSDoc on all public exports', tier: 'recommend', category: 'documentation' },
|
|
109
|
+
{ id: 'changelog', description: 'CHANGELOG.md', tier: 'recommend', category: 'documentation' },
|
|
110
|
+
{ id: 'test-edge-cases', description: 'Tests for boundary values, empty inputs, type mismatches', tier: 'recommend', category: 'testing' },
|
|
111
|
+
];
|
|
112
|
+
const LIBRARY_PACKAGE_SKIP = [
|
|
113
|
+
{ id: 'aria-attributes', description: 'ARIA (not relevant for libraries)', tier: 'skip', category: 'accessibility' },
|
|
114
|
+
{ id: 'responsive-layout', description: 'Responsive CSS (not relevant for libraries)', tier: 'skip', category: 'accessibility' },
|
|
115
|
+
{ id: 'dark-mode', description: 'Browser dark mode (not relevant for libraries)', tier: 'skip', category: 'polish' },
|
|
116
|
+
{ id: 'favicon', description: 'Favicons (not relevant for libraries)', tier: 'skip', category: 'polish' },
|
|
117
|
+
{ id: 'audio-feedback', description: 'Audio (not relevant for libraries)', tier: 'skip', category: 'polish' },
|
|
118
|
+
{ id: 'security-headers', description: 'HTTP security headers (not relevant for libraries)', tier: 'skip', category: 'security' },
|
|
119
|
+
{ id: 'css-custom-properties', description: 'CSS variables (not relevant for libraries)', tier: 'skip', category: 'polish' },
|
|
120
|
+
{ id: 'skip-link', description: 'Skip navigation (not relevant for libraries)', tier: 'skip', category: 'accessibility' },
|
|
121
|
+
{ id: 'theme-color-meta', description: 'Theme color meta tags (not relevant for libraries)', tier: 'skip', category: 'polish' },
|
|
122
|
+
];
|
|
123
|
+
// Build the full-stack map from backend + frontend.
|
|
124
|
+
// Enforce: union of both. Recommend: union of both. Skip: only items both agree are irrelevant.
|
|
125
|
+
function buildFullStackMap() {
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
const result = [];
|
|
128
|
+
// Enforce: union of backend + frontend enforce lists, deduplicated by id
|
|
129
|
+
for (const req of [...API_BACKEND_ENFORCE, ...FRONTEND_WEB_ENFORCE]) {
|
|
130
|
+
if (!seen.has(req.id)) {
|
|
131
|
+
seen.add(req.id);
|
|
132
|
+
result.push(req);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Recommend: union of backend + frontend recommend lists, deduplicated by id
|
|
136
|
+
for (const req of [...API_BACKEND_RECOMMEND, ...FRONTEND_WEB_RECOMMEND]) {
|
|
137
|
+
if (!seen.has(req.id)) {
|
|
138
|
+
seen.add(req.id);
|
|
139
|
+
result.push(req);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Skip: only items that appear in BOTH skip lists (intersection by id)
|
|
143
|
+
const backendSkipIds = new Set(API_BACKEND_SKIP.map(r => r.id));
|
|
144
|
+
for (const req of FRONTEND_WEB_SKIP) {
|
|
145
|
+
if (backendSkipIds.has(req.id) && !seen.has(req.id)) {
|
|
146
|
+
seen.add(req.id);
|
|
147
|
+
result.push(req);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return result;
|
|
151
|
+
}
|
|
152
|
+
exports.TIER_MAPS = {
|
|
153
|
+
'api-backend': [...API_BACKEND_ENFORCE, ...API_BACKEND_RECOMMEND, ...API_BACKEND_SKIP],
|
|
154
|
+
'frontend-web': [...FRONTEND_WEB_ENFORCE, ...FRONTEND_WEB_RECOMMEND, ...FRONTEND_WEB_SKIP],
|
|
155
|
+
'cli-tool': [...CLI_TOOL_ENFORCE, ...CLI_TOOL_RECOMMEND, ...CLI_TOOL_SKIP],
|
|
156
|
+
'library-package': [...LIBRARY_PACKAGE_ENFORCE, ...LIBRARY_PACKAGE_RECOMMEND, ...LIBRARY_PACKAGE_SKIP],
|
|
157
|
+
'full-stack': buildFullStackMap(),
|
|
158
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.URLShortener = exports.StatisticsTracker = void 0;
|
|
4
|
+
exports.validateURL = validateURL;
|
|
5
|
+
/**
|
|
6
|
+
* In-memory URL shortener primitives with URL validation, base62 short codes, and hit statistics.
|
|
7
|
+
*/
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const defaults_1 = require("./defaults");
|
|
10
|
+
const BASE62_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
11
|
+
function validateURL(value, maxLength = defaults_1.DEFAULT_URL_MAX_LENGTH) {
|
|
12
|
+
if (typeof value !== 'string') {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const normalizedURL = value.trim();
|
|
16
|
+
if (normalizedURL.length === 0 || normalizedURL.length > maxLength) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const parsedURL = new URL(normalizedURL);
|
|
21
|
+
return parsedURL.protocol === 'http:' || parsedURL.protocol === 'https:';
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
class StatisticsTracker {
|
|
28
|
+
hitCounts = new Map();
|
|
29
|
+
recordHit(shortCode) {
|
|
30
|
+
const normalizedCode = shortCode.trim();
|
|
31
|
+
if (normalizedCode.length === 0) {
|
|
32
|
+
throw new Error('Short code is required.');
|
|
33
|
+
}
|
|
34
|
+
const nextCount = this.getHits(normalizedCode) + 1;
|
|
35
|
+
this.hitCounts.set(normalizedCode, nextCount);
|
|
36
|
+
return nextCount;
|
|
37
|
+
}
|
|
38
|
+
getHits(shortCode) {
|
|
39
|
+
return this.hitCounts.get(shortCode.trim()) ?? 0;
|
|
40
|
+
}
|
|
41
|
+
reset(shortCode) {
|
|
42
|
+
if (typeof shortCode === 'string') {
|
|
43
|
+
const normalizedCode = shortCode.trim();
|
|
44
|
+
if (normalizedCode.length === 0) {
|
|
45
|
+
throw new Error('Short code is required.');
|
|
46
|
+
}
|
|
47
|
+
this.hitCounts.delete(normalizedCode);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.hitCounts.clear();
|
|
51
|
+
}
|
|
52
|
+
snapshot() {
|
|
53
|
+
return Object.fromEntries(this.hitCounts.entries());
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.StatisticsTracker = StatisticsTracker;
|
|
57
|
+
class URLShortener {
|
|
58
|
+
shortToURL = new Map();
|
|
59
|
+
urlToShort = new Map();
|
|
60
|
+
maxURLLength;
|
|
61
|
+
codeMinLength;
|
|
62
|
+
statistics;
|
|
63
|
+
constructor(options = {}) {
|
|
64
|
+
this.maxURLLength = options.maxURLLength ?? defaults_1.DEFAULT_URL_MAX_LENGTH;
|
|
65
|
+
this.codeMinLength = options.codeMinLength ?? defaults_1.DEFAULT_SHORT_CODE_MIN_LENGTH;
|
|
66
|
+
this.statistics = new StatisticsTracker();
|
|
67
|
+
}
|
|
68
|
+
shorten(url) {
|
|
69
|
+
const normalizedURL = this.normalizeURL(url);
|
|
70
|
+
const existingCode = this.urlToShort.get(normalizedURL);
|
|
71
|
+
if (existingCode) {
|
|
72
|
+
return existingCode;
|
|
73
|
+
}
|
|
74
|
+
let attempt = 0;
|
|
75
|
+
while (true) {
|
|
76
|
+
const shortCode = this.generateShortCode(normalizedURL, attempt);
|
|
77
|
+
const existingURL = this.shortToURL.get(shortCode);
|
|
78
|
+
if (!existingURL) {
|
|
79
|
+
this.shortToURL.set(shortCode, normalizedURL);
|
|
80
|
+
this.urlToShort.set(normalizedURL, shortCode);
|
|
81
|
+
return shortCode;
|
|
82
|
+
}
|
|
83
|
+
if (existingURL === normalizedURL) {
|
|
84
|
+
this.urlToShort.set(normalizedURL, shortCode);
|
|
85
|
+
return shortCode;
|
|
86
|
+
}
|
|
87
|
+
attempt += 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
resolve(shortCode) {
|
|
91
|
+
const normalizedCode = shortCode.trim();
|
|
92
|
+
if (normalizedCode.length === 0) {
|
|
93
|
+
throw new Error('Short code is required.');
|
|
94
|
+
}
|
|
95
|
+
const url = this.shortToURL.get(normalizedCode) ?? null;
|
|
96
|
+
if (url) {
|
|
97
|
+
this.statistics.recordHit(normalizedCode);
|
|
98
|
+
}
|
|
99
|
+
return url;
|
|
100
|
+
}
|
|
101
|
+
getHitCount(shortCode) {
|
|
102
|
+
return this.statistics.getHits(shortCode);
|
|
103
|
+
}
|
|
104
|
+
has(shortCode) {
|
|
105
|
+
return this.shortToURL.has(shortCode.trim());
|
|
106
|
+
}
|
|
107
|
+
normalizeURL(url) {
|
|
108
|
+
const normalizedURL = url.trim();
|
|
109
|
+
if (!validateURL(normalizedURL, this.maxURLLength)) {
|
|
110
|
+
throw new Error('A valid HTTP or HTTPS URL is required.');
|
|
111
|
+
}
|
|
112
|
+
return normalizedURL;
|
|
113
|
+
}
|
|
114
|
+
generateShortCode(url, attempt) {
|
|
115
|
+
const hashInput = attempt === 0 ? url : `${url}:${attempt}`;
|
|
116
|
+
const hashHex = (0, crypto_1.createHash)('sha256').update(hashInput).digest('hex').slice(0, 16);
|
|
117
|
+
const numericHash = BigInt(`0x${hashHex}`);
|
|
118
|
+
return this.encodeBase62(numericHash).padStart(this.codeMinLength, '0');
|
|
119
|
+
}
|
|
120
|
+
encodeBase62(value) {
|
|
121
|
+
if (value === 0n) {
|
|
122
|
+
return '0';
|
|
123
|
+
}
|
|
124
|
+
let remaining = value;
|
|
125
|
+
let encoded = '';
|
|
126
|
+
const radix = BigInt(BASE62_ALPHABET.length);
|
|
127
|
+
while (remaining > 0n) {
|
|
128
|
+
const index = Number(remaining % radix);
|
|
129
|
+
encoded = BASE62_ALPHABET[index] + encoded;
|
|
130
|
+
remaining /= radix;
|
|
131
|
+
}
|
|
132
|
+
return encoded;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
exports.URLShortener = URLShortener;
|